pax_global_header00006660000000000000000000000064151612465420014520gustar00rootroot0000000000000052 comment=4d3fcc513e0e2755b35be7430d5dd9779e64b52e icingadb-web-1.4.0/000077500000000000000000000000001516124654200140355ustar00rootroot00000000000000icingadb-web-1.4.0/AUTHORS000066400000000000000000000021511516124654200151040ustar00rootroot00000000000000Alexander A. Klimov Alvar Penning Eric Lippmann Feu Mourek Florian Strohmaier Johannes Meyer Johannes Rauh Jonada Hoxha Loei Petrus Marogi Lorenz Kästle marianrh <19990392+marianrh@users.noreply.github.com> Marius Hein Markus Opolka mrdsam <69315803+mrdsam@users.noreply.github.com> Noah Hilverling Patrick Dolinic Ravi Kumar Kempapura Srinivasa Sebastian Grund stupiddr Sukhwinder Dhillon Timm Ortloff Tobias Tiederle VerboEse Will_i_code <42240136+w1ll-i-code@users.noreply.github.com> Yonas Habteab icingadb-web-1.4.0/CHANGELOG.md000066400000000000000000000152761516124654200156610ustar00rootroot00000000000000# Icinga DB Web Changelog Please make sure to always read our [Upgrading](https://icinga.com/docs/icinga-db-web/latest/doc/05-Upgrading/) documentation before switching to a new version. ## 1.4.0 All included changes can be found on the milestone: https://github.com/Icinga/icingadb-web/milestone/14?closed=1 ### New Features * PHP 8.5 is now supported (#1315) * Exported SQL queries are now easier to copy (#991) * History queries can now disable the MySQL/MariaDB query optimizer via configuration to work around performance problems on affected database versions (#1219) * State pictograms now cover additional states and show tooltips to make them easier to understand (#1271, #1262) * SLA history can now be filtered by hostgroup (#1324) * `[INFO]` and `(INFO)` in plugin output are now rendered as visual state indicators instead of plain text (#1348) ### Fixes * Empty or incomplete object custom variables are handled correctly (#1332) * Setups without the `monitoring` module no longer produce unnecessary integration errors (#1346) * JSON exports with pagination now generate valid JSON without a leading comma (#1349) ## 1.3.0 (2025-11-19) All included changes can be found on the milestone: https://github.com/Icinga/icingadb-web/milestone/10?closed=1 ### Icinga Notifications This release accompanies the release of Icinga Notifications v0.2.0. We have changed how notification sources are configured, and now an integration into Icinga Web is mandatory. Icinga DB Web v1.3 will serve as the integration for Icinga DB v1.5. ### New Features * The *Services* tab of a host and the *History* tab of hosts and services are now filterable (#981, #1293) * Exporting hosts and services to CSV or JSON by using the `?columns=…` query parameter now includes only the columns specified in the parameter in the results (#1011) ### Breaking Changes * The restrictions `icingadb/denylist/variables` and `icingadb/protect/variables` from different roles are now merged into a single list, respectively. This means that variables denied in one role will not show up anymore if another role denies access to different variables. The same applies to `icingadb/protect/variables`, in which case variables protected in one role will now be protected even if another role protects different variables. This has been done to simplify the configuration and to get it more in line with how refusals work in Icinga Web. ### Maintenance * The migration widget in the upper right has now been removed. We assume most users may be rather annoyed by it, and those who are still unsure when to migrate fully to Icinga DB are unlikely to be finally convinced by it anyway. Modules with support for Icinga DB will now default to use it if available. * The upgrading documentation includes more details and other changes, make sure to read it carefully. ## 1.2.4 (2025-11-19) This release includes fixes for the following issues: * Commandtransport in cluster mode is broken #1292 * Redis config: Path "/nix/store" is not writable #1269 * Hide list selection-count and action list hint in minimal layout #1298 ## 1.2.3 (2025-10-16) **Notice:** This is a security release. It is recommended to upgrade _quickly_. See the related CVE: https://github.com/Icinga/icingadb-web/security/advisories/GHSA-w57j-28jc-8429 Included changes can be found on the milestone: https://github.com/Icinga/icingadb-web/milestone/13?closed=1 And a detailed description about the most important ones on our blog: https://icinga.com/blog/releasing-icinga-2-v2-15-1-2-14-7-and-2-13-13-and-icinga-db-web-v1-2-3-and-1-1-4/ ## 1.2.2 (2025-07-16) **Notice:** This is a security release. It is recommended to upgrade _quickly_. See the related CVE: https://github.com/Icinga/icingadb-web/security/advisories/GHSA-q2w7-mrx8-5473 Included changes can be found on the milestone: https://github.com/Icinga/icingadb-web/milestone/12?closed=1 And a detailed description about the most important ones on our blog: https://icinga.com/blog/releasing-icinga-web-2-v2-12-5-icinga-db-web-v1-2-2/ ## 1.2.1 (2025-06-30) This release [fixes an issue](https://github.com/Icinga/icingadb-web/pull/1229) with version comparison that caused incorrect detection of the Icinga DB version. As a result, the health check in Icinga Web will now accurately reflect the current Icinga DB version and no longer incorrectly indicate that an upgrade to version 1.4.0 is still required. ## 1.2.0 (2025-06-18) Included changes can be found on the milestone: https://github.com/Icinga/icingadb-web/milestone/7?closed=1 And a detailed description about the most important ones on our blog: https://icinga.com/blog/icinga-dependency-views/ ## 1.1.3 (2024-08-06) Included changes can be found on the milestone: https://github.com/Icinga/icingadb-web/milestone/8?closed=1 And a detailed description about the most important ones on our blog: https://icinga.com/blog/2024/08/06/releasing-icinga-db-web-v1-1-3-and-a-ipl-security-release/ ## 1.1.2 (2024-04-11) Included changes can be found on the milestone: https://github.com/Icinga/icingadb-web/milestone/6?closed=1 And a detailed description about the most important ones on our blog: https://icinga.com/blog/2024/04/11/releasing-icinga-db-1-2-0-and-icinga-db-web-1-1-2 ## 1.1.1 (2023-11-15) Included changes can be found on the milestone: https://github.com/Icinga/icingadb-web/milestone/5?closed=1 And a detailed description about the most important ones on our blog: https://icinga.com/blog/2023/11/16/releasing-icinga-db-web-v1-1-1-and-icinga-web-2-12-1/ ## 1.1.0 (2023-09-28) Included changes can be found on the milestone: https://github.com/Icinga/icingadb-web/milestone/2?closed=1 And a detailed description about the most important ones on our blog: https://icinga.com/blog/2023/09/28/releasing-icinga-db-web-v1-1/ ## 1.0.2 (2022-11-04) You can find all issues related to this release on the respective [Milestone](https://github.com/Icinga/icingadb-web/milestone/4?closed=1). Notable fixes in this release are that the *GenericTTS* module is now supported and that the legacy integration of modules with no official support for Icinga DB Web is working again, even if the *monitoring* module is disabled. Action and Note URLs, which disappeared with v1.0.1, are also visible again. Some enhancements also found their way in this release. They include improved compatibility with Icinga DB's asynchronous behavior and its migration tool included in the v1.1 release. ## 1.0.1 (2022-09-08) Here are Fixes: https://github.com/Icinga/icingadb-web/milestone/3?closed=1 Here someone blogged about them: https://icinga.com/blog/2022/09/08/releasing-icinga-db-web-v1-0-1/ ## 1.0.0 (2022-06-30) First stable release ## 1.0.0 RC2 (2021-11-12) Second release candidate ## 1.0.0 RC1 (2020-03-13) Initial release icingadb-web-1.4.0/CONTRIBUTING.md000066400000000000000000000142451516124654200162740ustar00rootroot00000000000000# Contributing Icinga is an open source project and lives from your ideas and contributions. There are many ways to contribute, from improving the documentation, submitting bug reports and features requests or writing code to add enhancements or fix bugs. #### Table of Contents 1. [Introduction](#introduction) 2. [Fork the Project](#fork-the-project) 3. [Branches](#branches) 4. [Commits](#commits) 5. [Pull Requests](#pull-requests) 6. [Testing](#testing) 7. [Source Code Patches](#source-code-patches) 8. [Documentation Patches](#documentation-patches) ## Introduction Please consider our [roadmap](https://github.com/Icinga/icingadb-web/milestones) and [open issues](https://github.com/icinga/icingadb-web/issues) when you start contributing to the project. Before starting your work on Icinga DB Web, you should [fork the project](https://help.github.com/articles/fork-a-repo/) to your GitHub account. This allows you to freely experiment with your changes. When your changes are complete, submit a [pull request](https://help.github.com/articles/using-pull-requests/). All pull requests will be reviewed and merged if they suit some general guidelines: * Changes are located in a topic branch * For new functionality, proper tests are written * Changes should follow the existing coding style and standards Please continue reading in the following sections for a step by step guide. ## Fork the Project [Fork the project](https://help.github.com/articles/fork-a-repo/) to your GitHub account and clone the repository: ``` git clone git@github.com:jdoe/icingadb-web.git cd icingadb-web ``` Add a new remote `upstream` with this repository as value. ``` git remote add upstream https://github.com/icinga/icingadb-web.git ``` You can pull updates to your fork's default branch: ``` git fetch --all git pull upstream HEAD ``` Please continue to learn about [branches](#branches). ## Branches Choosing a proper name for a branch helps us identify its purpose and possibly find an associated bug or feature. Generally a branch name should include a topic such as `fix` or `feature` followed by a description and an issue number if applicable. Branches should have only changes relevant to a specific issue. ``` git checkout -b fix/service-template-typo-1234 git checkout -b feature/config-handling-1235 ``` Continue to apply your changes and test them. More details on specific changes: * [Source Code Patches](#source-code-patches) * [Documentation Patches](#documentation-patches) ## Commits Once you've finished your work in a branch, please ensure to commit your changes. A good commit message includes a short topic, additional body and a reference to the issue you wish to solve (if existing). Fixes: ``` Fix missing style in detail view refs #4567 ``` Features: ``` Add DateTime picker refs #1234 ``` You can add multiple commits during your journey to finish your patch. Don't worry, you can squash those changes into a single commit later on. ## Pull Requests Once you've committed your changes, please update your local default branch and rebase your fix/feature branch against it before submitting a PR. ``` git checkout main git pull upstream HEAD git checkout fix/style-detail-view-5678 git rebase main ``` Once you've resolved any conflicts, push the branch to your remote repository. It might be necessary to force push after rebasing - use with care! New branch: ``` git push --set-upstream origin fix/style-detail-view-5678 ``` Existing branch: ``` git push -f origin fix/style-detail-view-5678 ``` You can now either use the [hub](https://hub.github.com) CLI tool to create a PR, or navigate to your GitHub repository and create a PR there. The pull request should again contain a telling subject and a reference with `fixes` to an existing issue id if any. That allows developers to automatically resolve the issues once your PR gets merged. ``` hub pull-request fixes #1234 ``` Thanks a lot for your contribution! ### Rebase a Branch If you accidentally sent in a PR which was not rebased against the upstream default branch, developers might ask you to rebase your PR. First off, fetch and pull the default branch. ``` git checkout main git fetch --all git pull upstream HEAD ``` Then change to your working branch and start rebasing it against the default: ``` git checkout fix/style-detail-view-5678 git rebase main ``` If you are running into a conflict, rebase will stop and ask you to fix the problems. ``` git status both modified: path/to/conflict.php ``` Edit the file and search for `>>>`. Fix, build, test and save as needed. Add the modified file(s) and continue rebasing. ``` git add path/to/conflict.php git rebase --continue ``` Once succeeded ensure to push your changed history remotely. ``` git push -f origin fix/style-detail-view-5678 ``` If you fear to break things, do the rebase in a backup branch first and later replace your current branch. ``` git checkout fix/style-detail-view-5678 git checkout -b fix/style-detail-view-5678-rebase git rebase main git branch -D fix/style-detail-view-5678 git checkout -b fix/style-detail-view-5678 git push -f origin fix/style-detail-view-5678 ``` ### Squash Commits > **Note:** > > Be careful with squashing. This might lead to non-recoverable mistakes. > > This is for advanced Git users. Say you want to squash the last 3 commits in your branch into a single one. Start an interactive (`-i`) rebase from current HEAD minus three commits (`HEAD~3`). ``` git rebase -i HEAD~3 ``` Git opens your preferred editor. `pick` the commit in the first line, change `pick` to `squash` on the other lines. ``` pick e4bf04e47 Fix style detail view squash d7b939d99 Tests squash b37fd5377 Doc updates ``` Save and let rebase to its job. Then force push the changes to the remote origin. ``` git push -f origin fix/style-detail-view-5678 ``` ## Testing TBD ## Source Code Patches Icinga DB Web is written in PHP, LESS and JavaScript. ## Documentation Patches The documentation is written in GitHub flavored [Markdown](https://guides.github.com/features/mastering-markdown/). It is located in the `doc/` directory and can be edited with your preferred editor. You can also edit it online on GitHub. ``` vim doc/02-Installation.md ``` icingadb-web-1.4.0/LICENSE.md000066400000000000000000001045131516124654200154450ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . icingadb-web-1.4.0/README.md000066400000000000000000000052511516124654200153170ustar00rootroot00000000000000# Icinga DB Web Icinga DB is a set of components for publishing, synchronizing and visualizing monitoring data in the Icinga ecosystem, consisting of: * Icinga DB Web which connects to both a Redis®[\*](doc/TRADEMARKS.md#redis) server and a database to view and work with most up-to-date monitoring data * Icinga 2 with its [Icinga DB feature](https://icinga.com/docs/icinga-2/latest/doc/14-features/#icinga-db) enabled, responsible for publishing the data to the Redis® server, i.e. configuration and its runtime updates, check results, state changes, downtimes, acknowledgements, notifications, and other events such as flapping * And the [Icinga DB daemon](https://icinga.com/docs/icinga-db), which synchronizes the data between the Redis® server and the database ![Icinga DB Architecture](doc/res/icingadb-architecture.png) ## Documentation Icinga DB Web documentation is available at [icinga.com/docs](https://icinga.com/docs/icinga-db-web/latest/). ## Features Icinga DB Web offers a modern and streamlined design to provide a clear and concise view of your monitoring environment, also with dark and light mode support. ![Icinga DB Dashboard](doc/res/icingadb-dashboard.png) ### Various List Layouts The view switcher allows to control the level of detail displayed in host and service list views: ![View Switcher Preview](doc/res/view-switcher-preview.png) ### Search with Autocomplete The search bar in list views can be used for everything from simple searches to creating complex filters. It allows full keyboard control and also supports contextual auto-completion. In addition, there is an editor for easier filter creation. ![Searchbar Completion Preview](doc/res/searchbar-completion-preview.png) ### Clean Detail Views Host and service detail views are structured to make best use of available space. Related information is grouped and important information is at the top for instant access without having to scroll down. ![Service Detail Preview](doc/res/service-detail-preview.png) ### Modal Dialogs Any interaction that requires user input, such as acknowledging problems, scheduling downtimes, etc., shows a modal dialog over the current view to preserve context and focus on interaction. ![Modal Dialog Preview](doc/res/modal-dialog-preview.png) ### Bulk Operations Bulk interactions such as scheduling downtimes for multiple objects, acknowledging multiple problems, etc. are easily accomplished with the `Continue With` control that operates on filtered lists. ![Continue With Preview](doc/res/continue-with-preview.png) ## License Icinga DB Web and the Icinga DB Web documentation are licensed under the terms of the [GNU General Public License Version 3](LICENSE.md). icingadb-web-1.4.0/SECURITY.md000066400000000000000000000004401516124654200156240ustar00rootroot00000000000000# Security Policy ## Supported Versions The latest two minors. If it's a critical threat and the latest minor is just a few weeks old, the third latest minor may also get an update. ## Reporting a Vulnerability Please head [here](https://icinga.com/company/contact/security-issues/). icingadb-web-1.4.0/application/000077500000000000000000000000001516124654200163405ustar00rootroot00000000000000icingadb-web-1.4.0/application/clicommands/000077500000000000000000000000001516124654200206315ustar00rootroot00000000000000icingadb-web-1.4.0/application/clicommands/MigrateCommand.php000066400000000000000000000754531516124654200242470ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Clicommands; use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Cli\Command; use Icinga\Data\ConfigObject; use Icinga\Exception\NotReadableError; use Icinga\Exception\NotWritableError; use Icinga\Module\Icingadb\Compat\UrlMigrator; use Icinga\Util\DirectoryIterator; use Icinga\Web\Request; use ipl\Stdlib\Str; use ipl\Web\Filter\QueryString; use ipl\Web\Url; class MigrateCommand extends Command { /** @var bool Skip the migration, only perform transformations */ protected $skipMigration = false; public function init(): void { Logger::getInstance()->setLevel(Logger::INFO); } /** * Migrate monitoring navigation items to Icinga DB Web * * USAGE * * icingacli icingadb migrate navigation [options] * * REQUIRED OPTIONS: * * --user= Migrate navigation items whose owner matches the given * name or owners matching the given pattern. Wildcard * matching by `*` possible. * * OPTIONS: * * --override Replace existing or already migrated items * (Attention: Actions are not backed up) * * --no-backup Remove monitoring actions and don't back up menu items */ public function navigationAction(): void { /** @var string $user */ $user = $this->params->getRequired('user'); $noBackup = $this->params->get('no-backup'); $preferencesPath = Config::resolvePath('preferences'); $sharedNavigation = Config::resolvePath('navigation'); if (! file_exists($preferencesPath) && ! file_exists($sharedNavigation)) { Logger::info('There are no user navigation items to migrate'); return; } $rc = 0; $directories = file_exists($preferencesPath) ? new DirectoryIterator($preferencesPath) : []; $anythingChanged = false; /** @var string $directory */ foreach ($directories as $directory) { /** @var string $username */ $username = $directories->key() === false ? '' : $directories->key(); if (fnmatch($user, $username) === false) { continue; } $menuItems = $this->readFromIni($directory . '/menu.ini', $rc); $hostActions = $this->readFromIni($directory . '/host-actions.ini', $rc); $serviceActions = $this->readFromIni($directory . '/service-actions.ini', $rc); $icingadbHostActions = $this->readFromIni($directory . '/icingadb-host-actions.ini', $rc); $icingadbServiceActions = $this->readFromIni($directory . '/icingadb-service-actions.ini', $rc); $menuUpdated = false; $originalMenuItems = $this->readFromIni($directory . '/menu.ini', $rc); Logger::info( 'Transforming legacy wildcard filters of existing Icinga DB Web items for user "%s"', $username ); if (! $menuItems->isEmpty()) { $menuUpdated = $this->transformNavigationItems($menuItems, $username, $rc); $anythingChanged |= $menuUpdated; } if (! $icingadbHostActions->isEmpty()) { $anythingChanged |= $this->transformNavigationItems($icingadbHostActions, $username, $rc); } if (! $icingadbServiceActions->isEmpty()) { $anythingChanged |= $this->transformNavigationItems( $icingadbServiceActions, $username, $rc ); } if (! $this->skipMigration) { Logger::info('Migrating monitoring navigation items for user "%s" to Icinga DB Web', $username); if (! $menuItems->isEmpty()) { $menuUpdated = $this->migrateNavigationItems($menuItems, $username, $directory . '/menu.ini', $rc); $anythingChanged |= $menuUpdated; } if (! $hostActions->isEmpty()) { $anythingChanged |= $this->migrateNavigationItems( $hostActions, $username, $directory . '/icingadb-host-actions.ini', $rc ); } if (! $serviceActions->isEmpty()) { $anythingChanged |= $this->migrateNavigationItems( $serviceActions, $username, $directory . '/icingadb-service-actions.ini', $rc ); } } if ($menuUpdated && ! $noBackup) { $this->createBackupIni("$directory/menu", $originalMenuItems); } } // Start migrating shared navigation items $menuItems = $this->readFromIni($sharedNavigation . '/menu.ini', $rc); $hostActions = $this->readFromIni($sharedNavigation . '/host-actions.ini', $rc); $serviceActions = $this->readFromIni($sharedNavigation . '/service-actions.ini', $rc); $icingadbHostActions = $this->readFromIni($sharedNavigation . '/icingadb-host-actions.ini', $rc); $icingadbServiceActions = $this->readFromIni($sharedNavigation . '/icingadb-service-actions.ini', $rc); $menuUpdated = false; $originalMenuItems = $this->readFromIni($sharedNavigation . '/menu.ini', $rc); Logger::info('Transforming legacy wildcard filters of existing shared Icinga DB Web navigation items'); if (! $menuItems->isEmpty()) { $menuUpdated = $this->transformNavigationItems($menuItems, $user, $rc); $anythingChanged |= $menuUpdated; } if (! $icingadbHostActions->isEmpty()) { $anythingChanged |= $this->transformNavigationItems($icingadbHostActions, $user, $rc); } if (! $icingadbServiceActions->isEmpty()) { $anythingChanged |= $this->transformNavigationItems( $icingadbServiceActions, $user, $rc ); } if (! $this->skipMigration) { Logger::info('Migrating shared monitoring navigation items to the Icinga DB Web items'); if (! $menuItems->isEmpty()) { $menuUpdated = $this->migrateNavigationItems($menuItems, $user, $sharedNavigation . '/menu.ini', $rc); $anythingChanged |= $menuUpdated; } if (! $hostActions->isEmpty()) { $anythingChanged |= $this->migrateNavigationItems( $hostActions, $user, $sharedNavigation . '/icingadb-host-actions.ini', $rc ); } if (! $serviceActions->isEmpty()) { $anythingChanged |= $this->migrateNavigationItems( $serviceActions, $user, $sharedNavigation . '/icingadb-service-actions.ini', $rc ); } } if ($menuUpdated && ! $noBackup) { $this->createBackupIni("$sharedNavigation/menu", $originalMenuItems); } if ($rc > 0) { if ($this->skipMigration) { Logger::error('Failed to transform some icingadb navigation items'); } else { Logger::error('Failed to migrate some monitoring navigation items'); } exit($rc); } if (! $anythingChanged) { Logger::info('Nothing to do'); } elseif ($this->skipMigration) { Logger::info('Successfully transformed all icingadb navigation item filters'); } else { Logger::info('Successfully migrated all monitoring navigation items'); } } /** * Migrate monitoring restrictions and permissions to Icinga DB Web * * Migrated roles do not grant general or full access to users afterward. * It is recommended to review any changes made by this command, before * manually granting access. * * USAGE * * icingacli icingadb migrate role [options] * * REQUIRED OPTIONS: (Use either, not both) * * --group= Update roles that are assigned to the given group or to * groups matching the pattern. Wildcard matching by `*` * possible. * * --role= Update role with the given name or roles whose names * match the pattern. Wildcard matching by `*` possible. * * OPTIONS: * * --override Reset any existing Icinga DB Web rules * * --no-backup Don't back up roles */ public function roleAction(): void { /** @var ?bool $override */ $override = $this->params->get('override'); $noBackup = $this->params->get('no-backup'); /** @var ?string $groupName */ $groupName = $this->params->get('group'); /** @var ?string $roleName */ $roleName = $this->params->get('role'); if ($roleName === null && $groupName === null) { $this->fail("One of the parameters 'group' or 'role' must be supplied"); } elseif ($roleName !== null && $groupName !== null) { $this->fail("Use either 'group' or 'role'. Both cannot be used as role overrules group."); } $rc = 0; $changed = false; $restrictions = Config::$configDir . '/roles.ini'; $rolesConfig = $this->readFromIni($restrictions, $rc); $monitoringRestriction = 'monitoring/filter/objects'; $monitoringPropertyBlackList = 'monitoring/blacklist/properties'; $icingadbRestrictions = [ 'objects' => 'icingadb/filter/objects', 'hosts' => 'icingadb/filter/hosts', 'services' => 'icingadb/filter/services' ]; $icingadbPropertyDenyList = 'icingadb/denylist/variables'; foreach ($rolesConfig as $name => $role) { /** @var string[] $role */ $role = iterator_to_array($role); if ($roleName === '*' || $groupName === '*') { $roleMatch = true; } elseif ($roleName !== null && fnmatch($roleName, $name)) { $roleMatch = true; } elseif ($groupName !== null && isset($role['groups'])) { $roleGroups = array_map('trim', explode(',', $role['groups'])); $roleMatch = false; foreach ($roleGroups as $roleGroup) { if (fnmatch($groupName, $roleGroup)) { $roleMatch = true; break; } } } else { $roleMatch = false; } if ($roleMatch && ! $this->skipMigration && $this->shouldUpdateRole($role, $override)) { if (isset($role[$monitoringRestriction])) { Logger::info( 'Migrating monitoring restriction filter for role "%s" to the Icinga DB Web restrictions', $name ); $transformedFilter = UrlMigrator::transformFilter( QueryString::parse($role[$monitoringRestriction]) ); if ($transformedFilter) { $role[$icingadbRestrictions['objects']] = QueryString::render($transformedFilter); $changed = true; } } if (isset($role[$monitoringPropertyBlackList])) { Logger::info( 'Migrating monitoring blacklisted properties for role "%s" to the Icinga DB Web deny list', $name ); $icingadbProperties = []; foreach (explode(',', $role[$monitoringPropertyBlackList]) as $property) { $icingadbProperties[] = preg_replace('/^(?:host|service)\.vars\./i', '', $property, 1); } $role[$icingadbPropertyDenyList] = str_replace( '**', '*', implode(',', array_unique($icingadbProperties)) ); $changed = true; } if (isset($role['permissions'])) { $updatedPermissions = []; Logger::info( 'Migrating monitoring permissions for role "%s" to the Icinga DB Web permissions', $name ); if (strpos($role['permissions'], 'monitoring')) { $monitoringProtection = Config::module('monitoring') ->get('security', 'protected_customvars'); if ($monitoringProtection !== null) { $role['icingadb/protect/variables'] = $monitoringProtection; $changed = true; } } foreach (explode(',', $role['permissions']) as $permission) { if (Str::startsWith($permission, 'icingadb/') || $permission === 'module/icingadb') { continue; } elseif (Str::startsWith($permission, 'monitoring/command/')) { $changed = true; $updatedPermissions[] = $permission; $updatedPermissions[] = str_replace('monitoring/', 'icingadb/', $permission); } elseif ($permission === 'no-monitoring/contacts') { $changed = true; $updatedPermissions[] = $permission; $role['icingadb/denylist/routes'] = 'contacts,contactgroups'; } else { $updatedPermissions[] = $permission; } } $role['permissions'] = implode(',', $updatedPermissions); } if (isset($role['refusals']) && is_string($role['refusals'])) { $updatedRefusals = []; Logger::info( 'Migrating monitoring refusals for role "%s" to the Icinga DB Web refusals', $name ); foreach (explode(',', $role['refusals']) as $refusal) { if (Str::startsWith($refusal, 'icingadb/') || $refusal === 'module/icingadb') { continue; } elseif (Str::startsWith($refusal, 'monitoring/command/')) { $changed = true; $updatedRefusals[] = $refusal; $updatedRefusals[] = str_replace('monitoring/', 'icingadb/', $refusal); } else { $updatedRefusals[] = $refusal; } } $role['refusals'] = implode(',', $updatedRefusals); } } if ($roleMatch) { foreach ($icingadbRestrictions as $object => $icingadbRestriction) { if (isset($role[$icingadbRestriction]) && is_string($role[$icingadbRestriction])) { $filter = QueryString::parse($role[$icingadbRestriction]); $filter = UrlMigrator::transformLegacyWildcardFilter($filter); if ($filter) { $filter = QueryString::render($filter); if ($filter !== $role[$icingadbRestriction]) { Logger::info( 'Icinga Db Web restriction of role "%s" for %s changed from "%s" to "%s"', $name, $object, $role[$icingadbRestriction], $filter ); $role[$icingadbRestriction] = $filter; $changed = true; } } } } } $rolesConfig->setSection($name, $role); } if ($changed) { if (! $noBackup) { $this->createBackupIni(Config::$configDir . '/roles'); } try { $rolesConfig->saveIni(); } catch (NotWritableError $error) { Logger::error($error); if ($this->skipMigration) { Logger::error('Failed to transform icingadb restrictions'); } else { Logger::error('Failed to migrate monitoring restrictions'); } exit(256); } if ($this->skipMigration) { Logger::info('Successfully transformed all icingadb restrictions'); } else { Logger::info('Successfully migrated monitoring restrictions and permissions in roles'); } } else { Logger::info('Nothing to do'); } } /** * Migrate monitoring dashboards to Icinga DB Web * * USAGE * * icingacli icingadb migrate dashboard [options] * * REQUIRED OPTIONS: * * --user= Migrate dashboards whose owner matches the given * name or owners matching the given pattern. Wildcard * matching by `*` possible. * * OPTIONS: * * --no-backup Don't back up dashboards */ public function dashboardAction(): void { /** @var string $user */ $user = $this->params->getRequired('user'); $noBackup = $this->params->get('no-backup'); $dashboardsPath = Config::resolvePath('dashboards'); if (! file_exists($dashboardsPath)) { Logger::info('There are no dashboards to migrate'); return; } $rc = 0; $directories = new DirectoryIterator($dashboardsPath); $anythingChanged = false; /** @var string $directory */ foreach ($directories as $directory) { /** @var string $userName */ $userName = $directories->key() === false ? '' : $directories->key(); if (fnmatch($user, $userName) === false) { continue; } $dashboardsConfig = $this->readFromIni($directory . '/dashboard.ini', $rc); $backupConfig = $this->readFromIni($directory . '/dashboard.ini', $rc); Logger::info( 'Migrating monitoring dashboards to Icinga DB Web dashboards for user "%s"', $userName ); $changed = false; /** @var ConfigObject $dashboardConfig */ foreach ($dashboardsConfig->getConfigObject() as $name => $dashboardConfig) { $dashboardUrlString = $dashboardConfig->get('url'); if ($dashboardUrlString !== null) { $dashBoardUrl = Url::fromPath($dashboardUrlString, [], new Request()); if (! $this->skipMigration && Str::startsWith(ltrim($dashboardUrlString, '/'), 'monitoring/')) { $dashboardConfig->url = UrlMigrator::transformUrl($dashBoardUrl)->getRelativeUrl(); $changed = true; } if (Str::startsWith(ltrim($dashboardUrlString, '/'), 'icingadb/')) { $finalUrl = $dashBoardUrl->onlyWith(['sort', 'limit', 'view', 'columns', 'page']); $params = $dashBoardUrl->without(['sort', 'limit', 'view', 'columns', 'page'])->getParams(); $filter = QueryString::parse($params->toString()); $filter = UrlMigrator::transformLegacyWildcardFilter($filter); if ($filter) { $oldFilterString = $params->toString(); $newFilterString = QueryString::render($filter); if ($oldFilterString !== $newFilterString) { Logger::info( 'Icinga Db Web filter of dashboard "%s" has changed from "%s" to "%s"', $name, $params->toString(), QueryString::render($filter) ); $finalUrl->setFilter($filter); $dashboardConfig->url = $finalUrl->getRelativeUrl(); $changed = true; } } } } } if ($changed && $noBackup === null) { $this->createBackupIni("$directory/dashboard", $backupConfig); } if ($changed) { $anythingChanged = true; } try { $dashboardsConfig->saveIni(); } catch (NotWritableError $error) { Logger::error($error); $rc = 256; } } if ($rc > 0) { if ($this->skipMigration) { Logger::error('Failed to transform some icingadb dashboards'); } else { Logger::error('Failed to migrate some monitoring dashboards'); } exit($rc); } if (! $anythingChanged) { Logger::info('Nothing to do'); } elseif ($this->skipMigration) { Logger::info('Successfully transformed all icingadb dashboards'); } else { Logger::info('Successfully migrated dashboards for all the matched users'); } } /** * Migrate Icinga DB Web wildcard filters of navigation items, dashboards and roles * * USAGE * * icingacli icingadb migrate filter * * OPTIONS: * * --no-backup Don't back up menu items, dashboards and roles */ public function filterAction(): void { $this->skipMigration = true; $this->params->set('user', '*'); $this->navigationAction(); $this->dashboardAction(); $this->params->set('role', '*'); $this->roleAction(); } private function transformNavigationItems(Config $config, string $owner, int &$rc): bool { $updated = false; /** @var ConfigObject $newConfigObject */ foreach ($config->getConfigObject() as $section => $newConfigObject) { $configOwner = $newConfigObject->get('owner') ?? ''; if ($configOwner && ! fnmatch($owner, $configOwner)) { continue; } if ( $newConfigObject->get('type') === 'icingadb-host-action' || $newConfigObject->get('type') === 'icingadb-service-action' ) { /** @var ?string $legacyFilter */ $legacyFilter = $newConfigObject->get('filter'); if ($legacyFilter !== null) { $filter = QueryString::parse($legacyFilter); $filter = UrlMigrator::transformLegacyWildcardFilter($filter); if ($filter) { $filter = QueryString::render($filter); if ($legacyFilter !== $filter) { $newConfigObject->filter = $filter; $updated = true; Logger::info( 'Icinga DB Web filter of action "%s" is changed from %s to "%s"', $section, $legacyFilter, $filter ); } } } } /** @var string $url */ $url = $newConfigObject->get('url'); if ($url && Str::startsWith(ltrim($url, '/'), 'icingadb/')) { $url = Url::fromPath($url, [], new Request()); $finalUrl = $url->onlyWith(['sort', 'limit', 'view', 'columns', 'page']); $params = $url->without(['sort', 'limit', 'view', 'columns', 'page'])->getParams(); $filter = QueryString::parse($params->toString()); $filter = UrlMigrator::transformLegacyWildcardFilter($filter); if ($filter) { $oldFilterString = $params->toString(); $newFilterString = QueryString::render($filter); if ($oldFilterString !== $newFilterString) { Logger::info( 'Icinga Db Web filter of navigation item "%s" has changed from "%s" to "%s"', $section, $oldFilterString, $newFilterString ); $newConfigObject->url = $finalUrl->setFilter($filter)->getRelativeUrl(); $updated = true; } } } } if ($updated) { try { $config->saveIni(); } catch (NotWritableError $error) { Logger::error($error); $rc = 256; return false; } } return $updated; } /** * Migrate the given config to the given new config path * * @param Config $config * @param string $owner * @param string $path * @param int $rc * * @return bool */ private function migrateNavigationItems(Config $config, string $owner, string $path, int &$rc): bool { $deleteLegacyFiles = $this->params->get('no-backup'); $override = $this->params->get('override'); $newConfig = $config->getConfigFile() === $path ? $config : $this->readFromIni($path, $rc); $updated = false; /** @var ConfigObject $configObject */ foreach ($config->getConfigObject() as $configObject) { $configOwner = $configObject->get('owner') ?? ''; if ($configOwner && ! fnmatch($owner, $configOwner)) { continue; } $migrateFilter = false; if ($configObject->type === 'host-action') { $updated = true; $migrateFilter = true; $configObject->type = 'icingadb-host-action'; } elseif ($configObject->type === 'service-action') { $updated = true; $migrateFilter = true; $configObject->type = 'icingadb-service-action'; } /** @var ?string $urlString */ $urlString = $configObject->get('url'); if ($urlString !== null) { $urlString = str_replace( ['$SERVICEDESC$', '$HOSTNAME$', '$HOSTADDRESS$', '$HOSTADDRESS6$'], ['$service.name$', '$host.name$', '$host.address$', '$host.address6$'], $urlString ); if ($urlString !== $configObject->url) { $configObject->url = $urlString; $updated = true; } $url = Url::fromPath($urlString, [], new Request()); try { $urlString = UrlMigrator::transformUrl($url)->getRelativeUrl(); $configObject->url = $urlString; $updated = true; } catch (\InvalidArgumentException $err) { // Do nothing } } /** @var ?string $legacyFilter */ $legacyFilter = $configObject->get('filter'); if ($migrateFilter && $legacyFilter) { $updated = true; $filter = QueryString::parse($legacyFilter); $filter = UrlMigrator::transformFilter($filter); if ($filter !== false) { $configObject->filter = QueryString::render($filter); } else { unset($configObject->filter); } } $section = $config->key(); if (! $newConfig->hasSection($section) || $newConfig === $config || $override) { $newConfig->setSection($section, $configObject); } } if ($updated) { try { $newConfig->saveIni(); // Remove the legacy file only if explicitly requested if ($deleteLegacyFiles && $newConfig !== $config) { unlink($config->getConfigFile()); } } catch (NotWritableError $error) { Logger::error($error); $rc = 256; return false; } } return $updated; } /** * Get the navigation items config from the given ini path * * @param string $path Absolute path of the ini file * @param int $rc The return code used to exit the action * * @return Config */ private function readFromIni($path, &$rc) { try { $config = Config::fromIni($path); } catch (NotReadableError $error) { Logger::error($error); $config = new Config(); $rc = 128; } return $config; } private function createBackupIni(string $path, ?Config $config = null): void { $counter = 0; while (true) { $filepath = $counter > 0 ? "$path.backup$counter.ini" : "$path.backup.ini"; if (! file_exists($filepath)) { if ($config) { $config->saveIni($filepath); } else { copy("$path.ini", $filepath); } break; } else { $counter++; } } } /** * Checks if the given role should be updated * * @param string[] $role * @param bool $override * * @return bool */ private function shouldUpdateRole(array $role, ?bool $override): bool { return ! ( isset($role['icingadb/filter/objects']) || isset($role['icingadb/filter/hosts']) || isset($role['icingadb/filter/services']) || isset($role['icingadb/denylist/routes']) || isset($role['icingadb/denylist/variables']) || isset($role['icingadb/protect/variables']) || (isset($role['permissions']) && str_contains($role['permissions'], 'icingadb')) ) || $override; } } icingadb-web-1.4.0/application/controllers/000077500000000000000000000000001516124654200207065ustar00rootroot00000000000000icingadb-web-1.4.0/application/controllers/CommandTransportController.php000066400000000000000000000117061516124654200267630ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Application\Config; use Icinga\Data\Filter\Filter; use Icinga\Forms\ConfirmRemovalForm; use Icinga\Module\Icingadb\Command\Transport\CommandTransportConfig; use Icinga\Module\Icingadb\Forms\ApiTransportForm; use Icinga\Module\Icingadb\Widget\ItemList\CommandTransportList; use Icinga\Web\Notification; use ipl\Html\HtmlString; use ipl\Web\Widget\ButtonLink; class CommandTransportController extends ConfigController { public function init() { $this->assertPermission('config/modules'); } public function indexAction() { $list = new CommandTransportList((new CommandTransportConfig())->select()); $this->addControl( (new ButtonLink( t('Create Command Transport'), 'icingadb/command-transport/add', 'plus' ))->setBaseTarget('_next') ); $this->addContent($list); $this->mergeTabs($this->Module()->getConfigTabs()); $this->getTabs()->disableLegacyExtensions(); $this->setTitle($this->getTabs() ->activate('command-transports') ->getActiveTab() ->getLabel()); } public function showAction() { $transportName = $this->params->getRequired('name'); $transportConfig = (new CommandTransportConfig()) ->select() ->where('name', $transportName) ->fetchRow(); if ($transportConfig === false) { $this->httpNotFound(t('Unknown transport')); } $form = new ApiTransportForm(); $form->populate((array) $transportConfig); $form->on(ApiTransportForm::ON_SUCCESS, function (ApiTransportForm $form) use ($transportName) { (new CommandTransportConfig())->update( 'transport', $form->getValues(), Filter::where('name', $transportName) ); Notification::success(sprintf(t('Updated command transport "%s" successfully'), $transportName)); $this->redirectNow('icingadb/command-transport'); }); $form->handleRequest(ServerRequest::fromGlobals()); $this->addContent($form); $this->addTitleTab($this->translate('Command Transport: %s'), $transportName); $this->getTabs()->disableLegacyExtensions(); } public function addAction() { $form = new ApiTransportForm(); $form->on(ApiTransportForm::ON_SUCCESS, function (ApiTransportForm $form) { (new CommandTransportConfig())->insert('transport', $form->getValues()); Notification::success(t('Created command transport successfully')); $this->redirectNow('icingadb/command-transport'); }); $form->handleRequest(ServerRequest::fromGlobals()); $this->addContent($form); $this->addTitleTab($this->translate('Add Command Transport')); $this->getTabs()->disableLegacyExtensions(); } public function removeAction() { $transportName = $this->params->getRequired('name'); $form = new ConfirmRemovalForm(); $form->setOnSuccess(function () use ($transportName) { (new CommandTransportConfig())->delete( 'transport', Filter::where('name', $transportName) ); Notification::success(sprintf(t('Removed command transport "%s" successfully'), $transportName)); $this->redirectNow('icingadb/command-transport'); }); $form->handleRequest(); $this->addContent(HtmlString::create($form->render())); $this->setTitle($this->translate('Remove Command Transport: %s'), $transportName); $this->getTabs()->disableLegacyExtensions(); } public function sortAction() { $transportName = $this->params->getRequired('name'); $newPosition = (int) $this->params->getRequired('pos'); $config = $this->Config('commandtransports'); if (! $config->hasSection($transportName)) { $this->httpNotFound(t('Unknown transport')); } if ($newPosition < 0 || $newPosition > $config->count()) { $this->httpBadRequest(t('Position out of bounds')); } $transports = $config->getConfigObject()->toArray(); $transportNames = array_keys($transports); array_splice($transportNames, array_search($transportName, $transportNames, true), 1); array_splice($transportNames, $newPosition, 0, [$transportName]); $sortedTransports = []; foreach ($transportNames as $name) { $sortedTransports[$name] = $transports[$name]; } $newConfig = Config::fromArray($sortedTransports); $newConfig->saveIni($config->getConfigFile()); $this->redirectNow('icingadb/command-transport'); } } icingadb-web-1.4.0/application/controllers/CommentController.php000066400000000000000000000025601516124654200250700ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use Icinga\Exception\NotFoundError; use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\CommentDetail; use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader; use ipl\Stdlib\Filter; class CommentController extends Controller { /** @var Comment The comment object */ protected $comment; public function init() { $this->addTitleTab(t('Comment')); $name = $this->params->getRequired('name'); $query = Comment::on($this->getDb())->with([ 'host', 'host.state', 'service', 'service.state', 'service.host', 'service.host.state' ]); $query->filter(Filter::equal('comment.name', $name)); $this->applyRestrictions($query); $comment = $query->first(); if ($comment === null) { throw new NotFoundError(t('Comment not found')); } $this->comment = $comment; } public function indexAction() { $this->addControl(new ObjectHeader($this->comment)); $this->addContent(new CommentDetail($this->comment)); $this->setAutorefreshInterval(10); } } icingadb-web-1.4.0/application/controllers/CommentsController.php000066400000000000000000000151421516124654200252530ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Common\CommandActions; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Forms\Command\Object\DeleteCommentForm; use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Orm\Model; use ipl\Orm\Query; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; class CommentsController extends Controller { use CommandActions; public function indexAction() { $this->addTitleTab(t('Comments')); $compact = $this->view->compact; $db = $this->getDb(); $comments = Comment::on($db)->with([ 'host', 'host.state', 'service', 'service.host', 'service.host.state', 'service.state' ]); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($comments); $sortControl = $this->createSortControl( $comments, [ 'comment.entry_time desc' => t('Entry Time'), 'host.display_name' => t('Host'), 'service.display_name' => t('Service'), 'comment.author' => t('Author'), 'comment.expire_time desc' => t('Expire Time') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $searchBar = $this->createSearchBar($comments, [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam() ]); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $this->filter($comments, $filter); $comments->peekAhead($compact); yield $this->export($comments); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $results = $comments->execute(); $continueWith = $this->createContinueWith(Links::commentsDetails(), $searchBar, $results->hasResult()); $this->addContent( (new ObjectList($results)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()) ); if ($compact) { $this->addContent( (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view']))) ->setBaseTarget('_next') ->setAttribute('title', sprintf( t('Show all %d comments'), $comments->count() )) ); } if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate($continueWith); } $this->setAutorefreshInterval(10); } public function deleteAction() { $this->assertIsGrantedOnCommandTargets('icingadb/command/comment/delete'); $this->setTitle(t('Remove Comments')); $this->handleCommandForm(DeleteCommentForm::class); } public function detailsAction() { $this->addTitleTab(t('Comments')); $db = $this->getDb(); $comments = Comment::on($db)->with([ 'host', 'host.state', 'service', 'service.host', 'service.host.state', 'service.state' ]); $comments->limit(3)->peekAhead(); $this->filter($comments); yield $this->export($comments); $rs = $comments->execute(); $this->addControl( (new ObjectList($rs)) ->setViewMode('minimal') ->setDetailActionsDisabled() ); $this->addControl(new ShowMore( $rs, Links::comments()->setFilter($this->getFilter()), sprintf(t('Show all %d comments'), $comments->count()) )); $this->addContent( (new DeleteCommentForm()) ->setObjects($comments) ->setAction( Links::commentsDelete() ->setFilter($this->getFilter()) ->getAbsoluteUrl() ) ); } public function completeAction() { $suggestions = new ObjectSuggestions(); $suggestions->setModel(Comment::class); $suggestions->forRequest(ServerRequest::fromGlobals()); $this->getDocument()->add($suggestions); } public function searchEditorAction() { $editor = $this->createSearchEditor(Comment::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } protected function getCommandTargetsUrl(): Url { return Url::fromPath('__CLOSE__'); } protected function fetchCommandTargets(): Query { $comments = Comment::on($this->getDb())->with([ 'host', 'host.state', 'service', 'service.host', 'service.host.state', 'service.state' ]); $this->filter($comments); return $comments; } public function isGrantedOn(string $permission, Model $object): bool { return parent::isGrantedOn($permission, $object->{$object->object_type}); } public function isGrantedOnType(string $permission, string $type, Filter\Rule $filter, bool $cache = true): bool { return parent::isGrantedOnType($permission, 'host', $filter, $cache) || parent::isGrantedOnType($permission, 'service', $filter, $cache); } } icingadb-web-1.4.0/application/controllers/ConfigController.php000066400000000000000000000030121516124654200246640ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use Icinga\Application\Config; use Icinga\Module\Icingadb\Forms\DatabaseConfigForm; use Icinga\Module\Icingadb\Forms\RedisConfigForm; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Web\Form; use Icinga\Web\Widget\Tab; use Icinga\Web\Widget\Tabs; use ipl\Html\HtmlString; class ConfigController extends Controller { public function init() { $this->assertPermission('config/modules'); parent::init(); } public function databaseAction() { $form = (new DatabaseConfigForm()) ->setIniConfig(Config::module('icingadb')); $form->handleRequest(); $this->mergeTabs($this->Module()->getConfigTabs()->activate('database')); $this->addFormToContent($form); } public function redisAction() { $form = (new RedisConfigForm()) ->setIniConfig($this->Config()); $form->handleRequest(); $this->mergeTabs($this->Module()->getConfigTabs()->activate('redis')); $this->addFormToContent($form); } protected function addFormToContent(Form $form) { $this->addContent(new HtmlString($form->render())); } protected function mergeTabs(Tabs $tabs): self { /** @var Tab $tab */ foreach ($tabs->getTabs() as $tab) { $this->tabs->add($tab->getName(), $tab); } return $this; } } icingadb-web-1.4.0/application/controllers/ContactController.php000066400000000000000000000024041516124654200250560ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use Icinga\Exception\NotFoundError; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader; use Icinga\Module\Icingadb\Widget\Detail\UserDetail; use ipl\Stdlib\Filter; class ContactController extends Controller { /** @var User The user object */ protected $user; public function init() { $this->assertRouteAccess('contacts'); $this->addTitleTab(t('Contact')); $name = $this->params->getRequired('name'); $query = User::on($this->getDb())->with('timeperiod'); $query->filter(Filter::equal('user.name', $name)); $this->applyRestrictions($query); $user = $query->first(); if ($user === null) { throw new NotFoundError(t('Contact not found')); } $this->user = $user; $this->setTitle($user->display_name); } public function indexAction() { $this->addControl(new ObjectHeader($this->user)); $this->addContent(new UserDetail($this->user)); $this->setAutorefreshInterval(10); } } icingadb-web-1.4.0/application/controllers/ContactgroupController.php000066400000000000000000000025211516124654200261330ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use Icinga\Exception\NotFoundError; use Icinga\Module\Icingadb\Model\Usergroup; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader; use Icinga\Module\Icingadb\Widget\Detail\UsergroupDetail; use ipl\Stdlib\Filter; class ContactgroupController extends Controller { /** @var Usergroup The usergroup object */ protected $usergroup; public function init() { $this->assertRouteAccess('contactgroups'); $this->addTitleTab(t('Contact Group')); $name = $this->params->getRequired('name'); $query = Usergroup::on($this->getDb()); $query->filter(Filter::equal('usergroup.name', $name)); $this->applyRestrictions($query); $usergroup = $query->first(); if ($usergroup === null) { throw new NotFoundError(t('Contact group not found')); } $this->usergroup = $usergroup; $this->setTitle($usergroup->display_name); } public function indexAction() { $this->addControl(new ObjectHeader($this->usergroup)); $this->addContent(new UsergroupDetail($this->usergroup)); $this->setAutorefreshInterval(10); } } icingadb-web-1.4.0/application/controllers/ContactgroupsController.php000066400000000000000000000056121516124654200263220ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Model\Usergroup; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; class ContactgroupsController extends Controller { public function init() { parent::init(); $this->assertRouteAccess(); } public function indexAction() { $this->addTitleTab(t('Contact Groups')); $db = $this->getDb(); $usergroups = Usergroup::on($db); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($usergroups); $sortControl = $this->createSortControl( $usergroups, [ 'usergroup.display_name' => t('Name') ] ); $searchBar = $this->createSearchBar($usergroups, [ $limitControl->getLimitParam(), $sortControl->getSortParam() ]); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $this->filter($usergroups, $filter); yield $this->export($usergroups); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($searchBar); $this->addContent( (new ObjectList($usergroups)) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()) ); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } $this->setAutorefreshInterval(10); } public function completeAction() { $suggestions = new ObjectSuggestions(); $suggestions->setModel(Usergroup::class); $suggestions->forRequest(ServerRequest::fromGlobals()); $this->getDocument()->add($suggestions); } public function searchEditorAction() { $editor = $this->createSearchEditor(Usergroup::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } } icingadb-web-1.4.0/application/controllers/ContactsController.php000066400000000000000000000056511516124654200252500ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; class ContactsController extends Controller { public function init() { parent::init(); $this->assertRouteAccess(); } public function indexAction() { $this->addTitleTab(t('Contacts')); $db = $this->getDb(); $users = User::on($db); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($users); $sortControl = $this->createSortControl( $users, [ 'user.display_name' => t('Name'), 'user.email' => t('Email'), 'user.pager' => t('Pager Address / Number') ] ); $searchBar = $this->createSearchBar($users, [ $limitControl->getLimitParam(), $sortControl->getSortParam() ]); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $this->filter($users, $filter); yield $this->export($users); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($searchBar); $this->addContent( (new ObjectList($users)) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()) ); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } $this->setAutorefreshInterval(10); } public function completeAction() { $suggestions = new ObjectSuggestions(); $suggestions->setModel(User::class); $suggestions->forRequest(ServerRequest::fromGlobals()); $this->getDocument()->add($suggestions); } public function searchEditorAction() { $editor = $this->createSearchEditor(User::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } } icingadb-web-1.4.0/application/controllers/DowntimeController.php000066400000000000000000000033171516124654200252550ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use Icinga\Exception\NotFoundError; use Icinga\Module\Icingadb\Model\Downtime; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\DowntimeDetail; use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader; use ipl\Stdlib\Filter; class DowntimeController extends Controller { /** @var Downtime */ protected $downtime; public function init() { $this->addTitleTab(t('Downtime')); $name = $this->params->getRequired('name'); $query = Downtime::on($this->getDb())->with([ 'host', 'host.state', 'service', 'service.state', 'service.host', 'service.host.state', 'parent', 'parent.host', 'parent.host.state', 'parent.service', 'parent.service.state', 'triggered_by', 'triggered_by.host', 'triggered_by.host.state', 'triggered_by.service', 'triggered_by.service.state' ]); $query->filter(Filter::equal('downtime.name', $name)); $this->applyRestrictions($query); $downtime = $query->first(); if ($downtime === null) { throw new NotFoundError(t('Downtime not found')); } $this->downtime = $downtime; } public function indexAction() { $detail = new DowntimeDetail($this->downtime); $this->addControl(new ObjectHeader($this->downtime)); $this->addContent($detail); $this->setAutorefreshInterval(10); } } icingadb-web-1.4.0/application/controllers/DowntimesController.php000066400000000000000000000164761516124654200254520ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Common\CommandActions; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Forms\Command\Object\DeleteDowntimeForm; use Icinga\Module\Icingadb\Model\Downtime; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Orm\Model; use ipl\Orm\Query; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; class DowntimesController extends Controller { use CommandActions; public function indexAction() { $this->addTitleTab(t('Downtimes')); $compact = $this->view->compact; $db = $this->getDb(); $downtimes = Downtime::on($db)->with([ 'host', 'host.state', 'service', 'service.host', 'service.host.state', 'service.state' ]); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($downtimes); $sortControl = $this->createSortControl( $downtimes, [ 'downtime.is_in_effect desc, downtime.start_time desc' => t('Is In Effect'), 'downtime.entry_time' => t('Entry Time'), 'host.display_name' => t('Host'), 'service.display_name' => t('Service'), 'downtime.author' => t('Author'), 'downtime.start_time desc' => t('Start Time'), 'downtime.end_time desc' => t('End Time'), 'downtime.scheduled_start_time desc' => t('Scheduled Start Time'), 'downtime.scheduled_end_time desc' => t('Scheduled End Time'), 'downtime.duration desc' => t('Duration') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $searchBar = $this->createSearchBar($downtimes, [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam() ]); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $this->filter($downtimes, $filter); $downtimes->peekAhead($compact); yield $this->export($downtimes); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $results = $downtimes->execute(); $continueWith = $this->createContinueWith(Links::downtimesDetails(), $searchBar, $results->hasResult()); $this->addContent( (new ObjectList($results)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()) ); if ($compact) { $this->addContent( (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view']))) ->setBaseTarget('_next') ->setAttribute('title', sprintf( t('Show all %d downtimes'), $downtimes->count() )) ); } if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate($continueWith); } $this->setAutorefreshInterval(10); } public function deleteAction() { $this->assertIsGrantedOnCommandTargets('icingadb/command/downtime/delete'); $this->setTitle(t('Cancel Downtimes')); $this->handleCommandForm(DeleteDowntimeForm::class); } public function detailsAction() { $this->addTitleTab(t('Downtimes')); $db = $this->getDb(); $downtimes = Downtime::on($db)->with([ 'host', 'host.state', 'service', 'service.host', 'service.host.state', 'service.state' ]); $downtimes->limit(3)->peekAhead(); $this->filter($downtimes); yield $this->export($downtimes); $rs = $downtimes->execute(); $this->addControl( (new ObjectList($rs)) ->setViewMode('minimal') ->setDetailActionsDisabled() ); $this->addControl(new ShowMore( $rs, Links::downtimes()->setFilter($this->getFilter()), sprintf(t('Show all %d downtimes'), $downtimes->count()) )); $this->addContent( (new DeleteDowntimeForm()) ->setObjects($downtimes) ->setAction( Links::downtimesDelete() ->setFilter($this->getFilter()) ->getAbsoluteUrl() ) ); } public function completeAction() { $suggestions = new ObjectSuggestions(); $suggestions->setModel(Downtime::class); $suggestions->forRequest(ServerRequest::fromGlobals()); $this->getDocument()->add($suggestions); } public function searchEditorAction() { $editor = $this->createSearchEditor(Downtime::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } protected function getCommandTargetsUrl(): Url { return Url::fromPath('__CLOSE__'); } protected function fetchCommandTargets(): Query { $downtimes = Downtime::on($this->getDb())->with([ 'host', 'host.state', 'service', 'service.host', 'service.host.state', 'service.state' ]); $this->filter($downtimes); return $downtimes; } public function isGrantedOn(string $permission, Model $object): bool { if ($object->scheduled_by !== null) { return false; } return parent::isGrantedOn($permission, $object->{$object->object_type}); } public function isGrantedOnType(string $permission, string $type, Filter\Rule $filter, bool $cache = true): bool { return parent::isGrantedOnType($permission, 'host', $filter, $cache) || parent::isGrantedOnType($permission, 'service', $filter, $cache); } } icingadb-web-1.4.0/application/controllers/ErrorController.php000066400000000000000000000054441516124654200245630ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use Icinga\Controllers\ErrorController as IcingaErrorController; use ipl\Html\Html; use ipl\Html\HtmlDocument; use ipl\Web\Layout\Content; use ipl\Web\Layout\Controls; use ipl\Web\Url; use ipl\Web\Widget\Link; use ipl\Web\Widget\Tabs; class ErrorController extends IcingaErrorController { /** @var HtmlDocument */ protected $document; /** @var Controls */ protected $controls; /** @var Content */ protected $content; /** @var Tabs */ protected $tabs; protected function prepareInit() { $this->document = new HtmlDocument(); $this->document->setSeparator("\n"); $this->controls = new Controls(); $this->content = new Content(); $this->tabs = new Tabs(); $this->controls->setTabs($this->tabs); $this->view->document = $this->document; } public function postDispatch() { $this->tabs->add(uniqid(), [ 'active' => true, 'label' => $this->view->title, 'url' => $this->getRequest()->getUrl() ]); if (! $this->content->isEmpty()) { $this->document->prepend($this->content); } if (! $this->view->compact && ! $this->controls->isEmpty()) { $this->document->prepend($this->controls); } parent::postDispatch(); } protected function postDispatchXhr() { parent::postDispatchXhr(); $this->getResponse()->setHeader('X-Icinga-Module', $this->getModuleName(), true); } public function errorAction() { $error = $this->getParam('error_handler'); $exception = $error->exception; /** @var \Exception $exception */ $message = $exception->getMessage(); if (substr($message, 0, 27) !== 'Cannot load resource config') { $this->forward('error', 'error', 'default'); return; } else { $this->setParam('error_handler', null); } // TODO: Find a native way for ipl-html to support enriching text with html $heading = Html::tag('h2', t('Database not configured')); $intro = Html::tag('p', ['data-base-target' => '_next'], Html::sprintf( 'You seem to not have configured a resource for Icinga DB yet. Please %s and then tell Icinga DB Web %s.', new Link( Html::tag('strong', 'create one'), Url::fromPath('config/resource') ), new Link( Html::tag('strong', 'which one it is'), Url::fromPath('icingadb/config/database') ) )); $this->content->add([$heading, $intro]); } } icingadb-web-1.4.0/application/controllers/EventController.php000066400000000000000000000034541516124654200245520ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\EventDetail; use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader; use ipl\Stdlib\Filter; class EventController extends Controller { /** @var History */ protected $event; public function init() { $this->addTitleTab(t('Event')); $id = $this->params->getRequired('id'); $query = History::on($this->getDb()) ->with([ 'host', 'host.state', 'service', 'service.state', 'comment', 'downtime', 'downtime.parent', 'downtime.parent.host', 'downtime.parent.host.state', 'downtime.parent.service', 'downtime.parent.service.state', 'downtime.triggered_by', 'downtime.triggered_by.host', 'downtime.triggered_by.host.state', 'downtime.triggered_by.service', 'downtime.triggered_by.service.state', 'flapping', 'notification', 'acknowledgement', 'state' ]) ->filter(Filter::equal('id', hex2bin($id))); $this->applyRestrictions($query); $event = $query->first(); if ($event === null) { $this->httpNotFound(t('Event not found')); } $this->event = $event; } public function indexAction() { $this->addControl(new ObjectHeader($this->event)); $this->addContent(new EventDetail($this->event)); } } icingadb-web-1.4.0/application/controllers/HealthController.php000066400000000000000000000111501516124654200246660ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Command\Instance\ToggleInstanceFeatureCommand; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Forms\Command\Instance\ToggleInstanceFeaturesForm; use Icinga\Module\Icingadb\Model\HoststateSummary; use Icinga\Module\Icingadb\Model\Instance; use Icinga\Module\Icingadb\Model\ServicestateSummary; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Health; use ipl\Web\Widget\VerticalKeyValue; use ipl\Html\Html; use ipl\Web\Url; class HealthController extends Controller { public function indexAction() { $this->addTitleTab(t('Health')); $db = $this->getDb(); $instance = Instance::on($db)->with(['endpoint']); $hoststateSummary = HoststateSummary::on($db); $servicestateSummary = ServicestateSummary::on($db); $this->applyRestrictions($hoststateSummary); $this->applyRestrictions($servicestateSummary); yield $this->export($instance, $hoststateSummary, $servicestateSummary); $instance = $instance->first(); if ($instance === null) { $this->addContent(Html::tag('p', t( 'It seems that Icinga DB is not running.' . ' Make sure Icinga DB is running and writing into the database.' ))); return; } $hoststateSummary = $hoststateSummary->first(); $servicestateSummary = $servicestateSummary->first(); $this->content->addAttributes(['class' => 'monitoring-health']); $this->addContent(new Health($instance)); $this->addContent(Html::tag('section', ['class' => 'check-summary'], [ Html::tag('div', ['class' => 'col'], [ Html::tag('h3', t('Host Checks')), Html::tag('div', ['class' => 'col-content'], [ new VerticalKeyValue( t('Active'), $hoststateSummary->hosts_active_checks_enabled ), new VerticalKeyValue( t('Passive'), $hoststateSummary->hosts_passive_checks_enabled ) ]) ]), Html::tag('div', ['class' => 'col'], [ Html::tag('h3', t('Service Checks')), Html::tag('div', ['class' => 'col-content'], [ new VerticalKeyValue( t('Active'), $servicestateSummary->services_active_checks_enabled ), new VerticalKeyValue( t('Passive'), $servicestateSummary->services_passive_checks_enabled ) ]) ]) ])); $featureCommands = Html::tag( 'section', ['class' => 'instance-commands'], Html::tag('h2', t('Feature Commands')) ); $toggleInstanceFeaturesCommandForm = new ToggleInstanceFeaturesForm([ ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS => $instance->icinga2_active_host_checks_enabled, ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS => $instance->icinga2_active_service_checks_enabled, ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS => $instance->icinga2_event_handlers_enabled, ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION => $instance->icinga2_flap_detection_enabled, ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS => $instance->icinga2_notifications_enabled, ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA => $instance->icinga2_performance_data_enabled ]); $toggleInstanceFeaturesCommandForm->setObjects([$instance]); $toggleInstanceFeaturesCommandForm->on(ToggleInstanceFeaturesForm::ON_SUCCESS, function () { $this->getResponse()->setAutoRefreshInterval(1); $this->redirectNow(Url::fromPath('icingadb/health')->getAbsoluteUrl()); }); $toggleInstanceFeaturesCommandForm->handleRequest(ServerRequest::fromGlobals()); $featureCommands->add($toggleInstanceFeaturesCommandForm); $this->addContent($featureCommands); $this->setAutorefreshInterval(30); } } icingadb-web-1.4.0/application/controllers/HistoryController.php000066400000000000000000000110201516124654200251160ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Util\OptimizerHints; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; class HistoryController extends Controller { public function indexAction() { $this->addTitleTab(t('History')); $compact = $this->view->compact; // TODO: Find a less-legacy way.. $preserveParams = [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM ]; $db = $this->getDb(); $history = History::on($db)->with([ 'host', 'host.state', 'service', 'service.state', 'comment', 'downtime', 'flapping', 'notification', 'acknowledgement', 'state' ]); $before = $this->params->shift('before', time()); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($history); $sortControl = $this->createSortControl( $history, [ 'history.event_time desc, history.event_type desc' => t('Event Time') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl, true); $searchBar = $this->createSearchBar($history, $preserveParams); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $history->peekAhead(); $page = $paginationControl->getCurrentPageNumber(); if ($page > 1 && ! $compact) { $history->resetOffset(); $history->limit($page * $limitControl->getLimit()); } $history->filter(Filter::lessThanOrEqual('event_time', $before)); $this->filter($history, $filter); $history->getWith()['history.host']->setJoinType('LEFT'); $history->filter(Filter::any( // Because of LEFT JOINs, make sure we'll fetch history entries only for items which still exist: Filter::like('host.id', '*'), Filter::like('service.id', '*') )); OptimizerHints::disableOptimizerForHistoryQueries($history); yield $this->export($history); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $url = Url::fromRequest() ->onlyWith($preserveParams) ->setFilter($filter); $historyList = (new LoadMoreObjectList($history->execute())) ->setPageSize($limitControl->getLimit()) ->setViewMode($viewModeSwitcher->getViewMode()) ->setLoadMoreUrl($url->setParam('before', $before)); if ($compact) { $historyList->setPageNumber($page); } if ($compact && $page > 1) { $this->document->addFrom($historyList); } else { $this->addContent($historyList); } if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } } public function completeAction() { $suggestions = new ObjectSuggestions(); $suggestions->setModel(History::class); $suggestions->forRequest(ServerRequest::fromGlobals()); $this->getDocument()->add($suggestions); } public function searchEditorAction() { $editor = $this->createSearchEditor(History::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } } icingadb-web-1.4.0/application/controllers/HostController.php000066400000000000000000000553771516124654200244210ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use ArrayIterator; use Generator; use Icinga\Exception\NotFoundError; use Icinga\Module\Icingadb\Command\Object\GetObjectCommand; use Icinga\Module\Icingadb\Command\Transport\CommandTransport; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Common\CommandActions; use Icinga\Module\Icingadb\Common\HostLinks; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Hook\TabHook\HookActions; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\ServicestateSummary; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Util\OptimizerHints; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\HostDetail; use Icinga\Module\Icingadb\Widget\Detail\HostInspectionDetail; use Icinga\Module\Icingadb\Widget\Detail\HostMetaInfo; use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader; use Icinga\Module\Icingadb\Widget\Detail\QuickActions; use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use ipl\Orm\Query; use ipl\Sql\Expression; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; use ipl\Web\Widget\Tabs; class HostController extends Controller { use CommandActions; use HookActions; /** @var Host The host object */ protected $host; public function init(): void { $name = $this->params->shiftRequired('name'); $query = Host::on($this->getDb())->with(['state', 'icon_image', 'timeperiod']); $query ->setResultSetClass(VolatileStateResults::class) ->filter(Filter::equal('host.name', $name)); $this->applyRestrictions($query); /** @var Host $host */ $host = $query->first(); if ($host === null) { throw new NotFoundError($this->translate('Host not found')); } $this->host = $host; $this->loadTabsForObject($host); $this->addControl(new ObjectHeader($host)); $this->setTitleTab($this->getRequest()->getActionName()); $this->setTitle($host->display_name); } public function indexAction(): void { $serviceSummary = ServicestateSummary::on($this->getDb()); $serviceSummary->filter(Filter::equal('service.host_id', $this->host->id)); $this->applyRestrictions($serviceSummary); if ($this->host->state->is_overdue) { $this->controls->addAttributes(['class' => 'overdue']); } $this->addControl(new HostMetaInfo($this->host)); $this->addControl(new QuickActions($this->host)); $this->addContent(new HostDetail($this->host, $serviceSummary->first())); $this->setAutorefreshInterval(10); } public function sourceAction(): void { $this->assertPermission('icingadb/object/show-source'); $apiResult = (new CommandTransport())->send( (new GetObjectCommand()) ->setObjects(new ArrayIterator([$this->host])) ); if ($this->host->state->is_overdue) { $this->controls->addAttributes(['class' => 'overdue']); } $this->addContent(new HostInspectionDetail( $this->host, reset($apiResult) )); } public function historyAction(): Generator { $compact = $this->view->compact; // TODO: Find a less-legacy way.. if ($this->host->state->is_overdue) { $this->controls->addAttributes(['class' => 'overdue']); } $db = $this->getDb(); $history = History::on($db)->with([ 'host', 'host.state', 'comment', 'downtime', 'flapping', 'notification', 'acknowledgement', 'state' ]); $history->filter(Filter::all( Filter::equal('history.host_id', $this->host->id), Filter::unlike('history.service_id', '*') )); $before = $this->params->shift('before', time()); $url = Url::fromRequest()->setParams(clone $this->params); $url->setParam('name', $this->host->name); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($history); $sortControl = $this->createSortControl( $history, [ 'history.event_time desc, history.event_type desc' => $this->translate('Event Time') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl, true); $preserveParams = [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'name' ]; $requestParams = Url::fromRequest()->onlyWith($preserveParams)->getParams(); $searchBar = $this->createSearchBar($history, $preserveParams) ->setEditorUrl( Url::fromPath('icingadb/host/history-search-editor') ->setParams($requestParams) )->setSuggestionUrl( Url::fromPath('icingadb/host/history-complete') ->setParams(clone $requestParams) ); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $history->peekAhead(); $page = $paginationControl->getCurrentPageNumber(); if ($page > 1 && ! $compact) { $history->resetOffset(); $history->limit($page * $limitControl->getLimit()); } $history->filter(Filter::lessThanOrEqual('event_time', $before)); $this->filter($history, $filter); OptimizerHints::disableOptimizerForHistoryQueries($history); yield $this->export($history); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $historyList = (new LoadMoreObjectList($history->execute())) ->setViewMode($viewModeSwitcher->getViewMode()) ->setPageSize($limitControl->getLimit()) ->setLoadMoreUrl($url->setParam('before', $before)->setFilter($filter)); if ($compact) { $historyList->setPageNumber($page); } if ($compact && $page > 1) { $this->document->addFrom($historyList); } else { $this->addContent($historyList); } if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } } public function servicesAction(): Generator { if ($this->host->state->is_overdue) { $this->controls->addAttributes(['class' => 'overdue']); } $db = $this->getDb(); $services = Service::on($db)->with([ 'state', 'state.last_comment', 'icon_image', 'host', 'host.state' ]); $services ->setResultSetClass(VolatileStateResults::class) ->filter(Filter::equal('host.id', $this->host->id)); $this->applyRestrictions($services); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($services); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $sortControl = $this->createSortControl( $services, [ 'service.display_name' => $this->translate('Name'), 'service.state.severity desc,service.state.last_state_change desc' => $this->translate('Severity'), 'service.state.soft_state' => $this->translate('Current State'), 'service.state.last_state_change desc' => $this->translate('Last State Change') ], ['service.state.severity DESC', 'service.state.last_state_change DESC'] ); $preserveParams = [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'name' ]; $requestParams = Url::fromRequest()->onlyWith($preserveParams)->getParams(); $searchBar = $this->createSearchBar($services, $preserveParams) ->setEditorUrl( Url::fromPath('icingadb/host/services-search-editor') ->setParams($requestParams) )->setSuggestionUrl( Url::fromPath('icingadb/host/services-complete') ->setParams(clone $requestParams) ); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $services->filter($filter); yield $this->export($services); $results = $services->execute(); $serviceList = (new ObjectList($results)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $continueWith = $this->createContinueWith( Links::servicesDetails()->setFilter(Filter::equal('host.name', $this->host->name)), $searchBar, $results->hasResult() ); $this->addContent($serviceList); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate($continueWith); } $this->setAutorefreshInterval(10); } public function parentsAction(): Generator { $nodesQuery = $this->fetchDependencyNodes(true); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($nodesQuery); $sortControl = $this->createSortControl( $nodesQuery, [ 'name' => $this->translate('Name'), 'severity desc, last_state_change desc' => $this->translate('Severity'), 'state' => $this->translate('Current State'), 'last_state_change desc' => $this->translate('Last State Change') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $searchBar = $this->createSearchBar( $nodesQuery, [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'name' ] ); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $nodesQuery->filter($filter); yield $this->export($nodesQuery); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $this->addContent( (new ObjectList($nodesQuery)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()) ); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } $this->setAutorefreshInterval(10); } public function childrenAction(): Generator { $nodesQuery = $this->fetchDependencyNodes(); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($nodesQuery); $sortControl = $this->createSortControl( $nodesQuery, [ 'name' => $this->translate('Name'), 'severity desc, last_state_change desc' => $this->translate('Severity'), 'state' => $this->translate('Current State'), 'last_state_change desc' => $this->translate('Last State Change') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $preserveParams = [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'name' ]; $requestParams = Url::fromRequest()->onlyWith($preserveParams)->getParams(); $searchBar = $this->createSearchBar($nodesQuery, $preserveParams) ->setEditorUrl( Url::fromPath('icingadb/host/children-search-editor') ->setParams($requestParams) )->setSuggestionUrl( Url::fromPath('icingadb/host/children-complete') ->setParams(clone $requestParams) ); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $nodesQuery->filter($filter); yield $this->export($nodesQuery); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $this->addContent( (new ObjectList($nodesQuery)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()) ); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } $this->setAutorefreshInterval(10); } public function completeAction(): void { $suggestions = (new ObjectSuggestions()) ->setModel(DependencyNode::class) ->onlyWithCustomVarSources(['host', 'service', 'hostgroup', 'servicegroup']) ->setBaseFilter(Filter::equal("child.host.id", $this->host->id)) ->forRequest($this->getServerRequest()); $this->getDocument()->add($suggestions); } public function childrenCompleteAction(): void { $suggestions = (new ObjectSuggestions()) ->setModel(DependencyNode::class) ->onlyWithCustomVarSources(['host', 'service', 'hostgroup', 'servicegroup']) ->setBaseFilter(Filter::equal("parent.host.id", $this->host->id)) ->forRequest($this->getServerRequest()); $this->getDocument()->add($suggestions); } public function historyCompleteAction(): void { $suggestions = (new ObjectSuggestions()) ->setModel(History::class) ->setBaseFilter(Filter::equal('host.id', $this->host->id)) ->forRequest($this->getServerRequest()); $this->getDocument()->addHtml($suggestions); } public function servicesCompleteAction(): void { $suggestions = (new ObjectSuggestions()) ->setModel(Service::class) ->setBaseFilter(Filter::equal('host.id', $this->host->id)) ->forRequest($this->getServerRequest()); $this->getDocument()->addHtml($suggestions); } public function searchEditorAction(): void { $editor = $this->createSearchEditor( DependencyNode::on($this->getDb()), Url::fromPath('icingadb/host/parents', ['name' => $this->host->name]), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'name' ] ); $this->getDocument()->add($editor); $this->setTitle($this->translate('Adjust Filter')); } public function childrenSearchEditorAction(): void { $preserveParams = [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'name' ]; $editor = $this->createSearchEditor( DependencyNode::on($this->getDb()), Url::fromPath('icingadb/host/children', ['name' => $this->host->name]), $preserveParams ); $editor->setSuggestionUrl( Url::fromPath('icingadb/host/children-complete') ->setParams(Url::fromRequest()->onlyWith($preserveParams)->getParams()) ); $this->getDocument()->add($editor); $this->setTitle($this->translate('Adjust Filter')); } public function historySearchEditorAction(): void { $preserveParams = [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'name' ]; $editor = $this->createSearchEditor( History::on($this->getDb()), Url::fromPath('icingadb/host/history', ['name' => $this->host->name]), $preserveParams ); $editor->setSuggestionUrl( Url::fromPath('icingadb/host/history-complete') ->setParams(Url::fromRequest()->onlyWith($preserveParams)->getParams()) ); $this->getDocument()->addHtml($editor); $this->setTitle($this->translate('Adjust Filter')); } public function servicesSearchEditorAction(): void { $preserveParams = [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'name' ]; $editor = $this->createSearchEditor( Service::on($this->getDb()), Url::fromPath('icingadb/host/services', ['name' => $this->host->name]), $preserveParams ); $editor->setSuggestionUrl( Url::fromPath('icingadb/host/services-complete') ->setParams(Url::fromRequest()->onlyWith($preserveParams)->getParams()) ); $this->getDocument()->addHtml($editor); $this->setTitle($this->translate('Adjust Filter')); } /** * Fetch the dependency nodes of the current host * * @param bool $parents Whether to fetch the parents or the children * * @return Query */ protected function fetchDependencyNodes(bool $parents = false): Query { $query = DependencyNode::forHost($this->host->id, $this->getDb(), $parents) ->with([ 'host', 'host.state', 'host.state.last_comment', 'service', 'service.state', 'service.state.last_comment', 'service.host', 'service.host.state', 'redundancy_group', 'redundancy_group.state' ]) ->setResultSetClass(VolatileStateResults::class); $this->applyRestrictions($query); return $query; } protected function createTabs(): Tabs { if (! Backend::supportsDependencies()) { $hasDependencyNode = false; } else { $hasDependencyNode = DependencyNode::on($this->getDb()) ->columns([new Expression('1')]) ->filter(Filter::all( Filter::equal('host_id', $this->host->id), Filter::unlike('service_id', '*') )) ->disableDefaultSort() ->first() !== null; } $tabs = $this->getTabs() ->add('index', [ 'label' => $this->translate('Host'), 'url' => Links::host($this->host) ]) ->add('services', [ 'label' => $this->translate('Services'), 'url' => HostLinks::services($this->host) ]) ->add('history', [ 'label' => $this->translate('History'), 'url' => HostLinks::history($this->host) ]); if ($hasDependencyNode) { $tabs->add('parents', [ 'label' => $this->translate('Parents'), 'url' => Url::fromPath('icingadb/host/parents', ['name' => $this->host->name]) ])->add('children', [ 'label' => $this->translate('Children'), 'url' => Url::fromPath('icingadb/host/children', ['name' => $this->host->name]) ]); } if ($this->hasPermission('icingadb/object/show-source')) { $tabs->add('source', [ 'label' => $this->translate('Source'), 'url' => Links::hostSource($this->host) ]); } foreach ($this->loadAdditionalTabs() as $name => $tab) { $tabs->add($name, $tab + ['urlParams' => ['name' => $this->host->name]]); } return $tabs; } protected function setTitleTab(string $name): void { $tab = $this->createTabs()->get($name); if ($tab !== null) { $this->getTabs()->activate($name); } } protected function fetchCommandTargets(): array { return [$this->host]; } protected function getCommandTargetsUrl(): Url { return Links::host($this->host); } protected function getDefaultTabControls(): array { return [new ObjectHeader($this->host)]; } } icingadb-web-1.4.0/application/controllers/HostgroupController.php000066400000000000000000000130061516124654200254550ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use Generator; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Hostgroupsummary; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Widget\ItemTable\HostgroupTableRow; use ipl\Html\Html; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; class HostgroupController extends Controller { /** @var string */ protected $hostgroupName; public function init() { $this->assertRouteAccess('hostgroups'); $this->hostgroupName = $this->params->shiftRequired('name'); } /** * Fetch the host group object * * @return Hostgroupsummary */ protected function fetchHostgroup(): Hostgroupsummary { $query = Hostgroupsummary::on($this->getDb()); foreach ($query->getUnions() as $unionPart) { $unionPart->filter(Filter::equal('hostgroup.name', $this->hostgroupName)); } $this->applyRestrictions($query); /** @var Hostgroupsummary $hostgroup */ $hostgroup = $query->first(); if ($hostgroup === null) { $this->httpNotFound(t('Host group not found')); } return $hostgroup; } public function indexAction(): Generator { $db = $this->getDb(); $hostgroup = $this->fetchHostgroup(); $hosts = Host::on($db)->with(['state', 'state.last_comment', 'icon_image']); $hosts ->setResultSetClass(VolatileStateResults::class) ->filter(Filter::equal('hostgroup.id', $hostgroup->id)); $this->applyRestrictions($hosts); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($hosts); $sortControl = $this->createSortControl( $hosts, [ 'host.display_name' => t('Name'), 'host.state.severity desc,host.state.last_state_change desc' => t('Severity'), 'host.state.soft_state' => t('Current State'), 'host.state.last_state_change desc' => t('Last State Change') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $searchBar = $this->createSearchBar($hosts, [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'name' ]); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $hosts->filter($filter); yield $this->export($hosts); $results = $hosts->execute(); $hostList = (new ObjectList($results)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()); // ICINGAWEB_EXPORT_FORMAT is not set yet and $this->format is inaccessible, yeah... if ($this->getRequest()->getParam('format') === 'pdf') { $this->addContent(new ObjectHeader($hostgroup)); $this->addContent(Html::tag('h2', null, t('Hosts'))); } else { $this->addControl(new ObjectHeader($hostgroup)); } $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $continueWith = $this->createContinueWith( Links::hostsDetails() ->setFilter(Filter::equal('hostgroup.name', $hostgroup->name)), $searchBar, $results->hasResult() ); $this->addContent($hostList); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate($continueWith); } $this->addTitleTab(t('Host Group')); $this->setTitle($hostgroup->display_name); $this->setAutorefreshInterval(10); } public function completeAction(): void { $suggestions = new ObjectSuggestions(); $suggestions->setModel(Host::class); $suggestions->setBaseFilter(Filter::equal('hostgroup.name', $this->hostgroupName)); $suggestions->forRequest($this->getServerRequest()); $this->getDocument()->add($suggestions); } public function searchEditorAction(): void { $editor = $this->createSearchEditor(Host::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'name' ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } } icingadb-web-1.4.0/application/controllers/HostgroupsController.php000066400000000000000000000115251516124654200256440ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Model\Hostgroup; use Icinga\Module\Icingadb\Model\Hostgroupsummary; use Icinga\Module\Icingadb\View\HostgroupGridRenderer; use Icinga\Module\Icingadb\View\HostgroupRenderer; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Widget\ItemTable\ObjectGrid; use Icinga\Module\Icingadb\Widget\ItemTable\ObjectTable; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; class HostgroupsController extends Controller { public function init() { parent::init(); $this->assertRouteAccess(); } public function indexAction() { $this->addTitleTab(t('Host Groups')); $compact = $this->view->compact; $db = $this->getDb(); $hostgroups = Hostgroupsummary::on($db); $this->handleSearchRequest($hostgroups); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($hostgroups); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); if ($viewModeSwitcher->getViewMode() === 'grid') { $hostgroups->without([ 'services_critical_handled', 'services_critical_unhandled', 'services_ok', 'services_pending', 'services_total', 'services_unknown_handled', 'services_unknown_unhandled', 'services_warning_handled', 'services_warning_unhandled', ]); } $sortControl = $this->createSortControl( $hostgroups, [ 'display_name' => t('Name'), 'hosts_severity desc, display_name' => t('Severity'), 'hosts_total desc' => t('Total Hosts'), ], ['hosts_severity DESC', 'display_name'] ); $searchBar = $this->createSearchBar($hostgroups, [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam() ]); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $this->filter($hostgroups, $filter); $hostgroups->peekAhead($compact); yield $this->export($hostgroups); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $results = $hostgroups->execute(); if ($viewModeSwitcher->getViewMode() === 'grid') { $content = new ObjectGrid($results, (new HostgroupGridRenderer())->setBaseFilter($filter)); } else { $content = new ObjectTable($results, (new HostgroupRenderer())->setBaseFilter($filter)); } $content->setEmptyStateMessage($paginationControl->getEmptyStateMessage()); $this->addContent($content); if ($compact) { $this->addContent( (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view']))) ->setBaseTarget('_next') ->setAttribute('title', sprintf( t('Show all %d hostgroups'), $hostgroups->count() )) ); } if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } $this->setAutorefreshInterval(30); } public function completeAction() { $suggestions = new ObjectSuggestions(); $suggestions->setModel(Hostgroup::class); $suggestions->forRequest(ServerRequest::fromGlobals()); $this->getDocument()->add($suggestions); } public function searchEditorAction() { $editor = $this->createSearchEditor(Hostgroupsummary::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } } icingadb-web-1.4.0/application/controllers/HostsController.php000066400000000000000000000204271516124654200245700ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Common\CommandActions; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\HoststateSummary; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Util\FeatureStatus; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions; use Icinga\Module\Icingadb\Widget\Detail\ObjectsDetail; use Icinga\Module\Icingadb\Widget\HostStatusBar; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Widget\ItemTable\HostItemTable; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Orm\Query; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; class HostsController extends Controller { use CommandActions; public function indexAction() { $this->addTitleTab(t('Hosts')); $compact = $this->view->compact; $db = $this->getDb(); $hosts = Host::on($db)->with(['state', 'icon_image', 'state.last_comment']); $hosts->getWith()['host.state']->setJoinType('INNER'); $hosts->setResultSetClass(VolatileStateResults::class); $this->handleSearchRequest($hosts, ['address', 'address6']); $summary = null; if (! $compact) { $summary = HoststateSummary::on($db); } $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($hosts); $sortControl = $this->createSortControl( $hosts, [ 'host.display_name' => t('Name'), 'host.state.severity desc,host.state.last_state_change desc' => t('Severity'), 'host.state.soft_state' => t('Current State'), 'host.state.last_state_change desc' => t('Last State Change') ], ['host.state.severity DESC', 'host.state.last_state_change DESC'] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $columns = $this->createColumnControl($hosts, $viewModeSwitcher); $searchBar = $this->createSearchBar($hosts, [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'columns' ]); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $hosts->peekAhead($compact); $this->filter($hosts, $filter); if (! $compact) { $this->filter($summary, $filter); yield $this->export($hosts, $summary); } else { yield $this->export($hosts); } $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $results = $hosts->execute(); $continueWith = $this->createContinueWith(Links::hostsDetails(), $searchBar, $results->hasResult()); if ($viewModeSwitcher->getViewMode() === 'tabular') { $hostList = (new HostItemTable($results, HostItemTable::applyColumnMetaData($hosts, $columns))) ->setSort($sortControl->getSort()); } else { $hostList = (new ObjectList($results)) ->setViewMode($viewModeSwitcher->getViewMode()); } $hostList->setEmptyStateMessage($paginationControl->getEmptyStateMessage()); $this->addContent($hostList); if ($compact) { $this->addContent( (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view']))) ->setBaseTarget('_next') ->setAttribute('title', sprintf( t('Show all %d hosts'), $hosts->count() )) ); } else { /** @var HoststateSummary $hostsSummary */ $hostsSummary = $summary->first(); $this->addFooter((new HostStatusBar($hostsSummary))->setBaseFilter($filter)); } if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate($continueWith); } $this->setAutorefreshInterval(10); } public function detailsAction() { $this->addTitleTab(t('Hosts')); $db = $this->getDb(); $hosts = Host::on($db)->with(['state', 'icon_image']); $hosts->setResultSetClass(VolatileStateResults::class); $summary = HoststateSummary::on($db)->with(['state']); $this->filter($hosts); $this->filter($summary); $hosts->limit(3); $hosts->peekAhead(); yield $this->export($hosts, $summary); $results = $hosts->execute(); $summary = $summary->first(); $downtimes = Host::on($db)->with(['downtime']); $downtimes->getWith()['host.downtime']->setJoinType('INNER'); $this->filter($downtimes); $summary->downtimes_total = $downtimes->count(); $comments = Host::on($db)->with(['comment']); $comments->getWith()['host.comment']->setJoinType('INNER'); // TODO: This should be automatically done by the model/resolver and added as ON condition $comments->filter(Filter::equal('comment.object_type', 'host')); $this->filter($comments); $summary->comments_total = $comments->count(); $this->addControl( (new ObjectList($results)) ->setViewMode('minimal') ->setDetailActionsDisabled() ); $this->addControl(new ShowMore( $results, Links::hosts()->setFilter($this->getFilter()), sprintf(t('Show all %d hosts'), $hosts->count()) )); $this->addControl( (new MultiselectQuickActions('host', $summary)) ->setBaseFilter($this->getFilter()) ); $this->addContent( (new ObjectsDetail('host', $summary, $hosts)) ->setBaseFilter($this->getFilter()) ); } public function completeAction() { $suggestions = new ObjectSuggestions(); $suggestions->setModel(Host::class); $suggestions->forRequest(ServerRequest::fromGlobals()); $this->getDocument()->add($suggestions); } public function searchEditorAction() { $editor = $this->createSearchEditor(Host::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'columns' ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } protected function fetchCommandTargets(): Query { $db = $this->getDb(); $hosts = Host::on($db)->with('state'); $hosts->setResultSetClass(VolatileStateResults::class); switch ($this->getRequest()->getActionName()) { case 'acknowledge': $hosts->filter(Filter::equal('state.is_problem', 'y')) ->filter(Filter::equal('state.is_acknowledged', 'n')); break; } $this->filter($hosts); return $hosts; } protected function getCommandTargetsUrl(): Url { return Links::hostsDetails()->setFilter($this->getFilter()); } protected function getFeatureStatus() { $summary = HoststateSummary::on($this->getDb()); $this->filter($summary); return new FeatureStatus('host', $summary->first()); } } icingadb-web-1.4.0/application/controllers/NotificationsController.php000066400000000000000000000107571516124654200263060ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Model\NotificationHistory; use Icinga\Module\Icingadb\Util\OptimizerHints; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; class NotificationsController extends Controller { public function indexAction() { $this->addTitleTab(t('Notifications')); $compact = $this->view->compact; $preserveParams = [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM ]; $db = $this->getDb(); $notifications = NotificationHistory::on($db)->with([ 'history', 'host', 'host.state', 'service', 'service.state' ]); $this->handleSearchRequest($notifications); $before = $this->params->shift('before', time()); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($notifications); $sortControl = $this->createSortControl( $notifications, [ 'notification_history.send_time desc' => t('Send Time') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl, true); $searchBar = $this->createSearchBar($notifications, $preserveParams); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $notifications->peekAhead(); $page = $paginationControl->getCurrentPageNumber(); if ($page > 1 && ! $compact) { $notifications->resetOffset(); $notifications->limit($page * $limitControl->getLimit()); } $notifications->filter(Filter::lessThanOrEqual('send_time', $before)); $this->filter($notifications, $filter); $notifications->filter(Filter::any( // Make sure we'll fetch service history entries only for services which still exist Filter::unlike('service_id', '*'), Filter::like('history.service.id', '*') )); OptimizerHints::disableOptimizerForHistoryQueries($notifications); yield $this->export($notifications); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $url = Url::fromRequest() ->onlyWith($preserveParams) ->setFilter($filter); $notificationList = (new LoadMoreObjectList($notifications->execute())) ->setPageSize($limitControl->getLimit()) ->setViewMode($viewModeSwitcher->getViewMode()) ->setLoadMoreUrl($url->setParam('before', $before)); if ($compact) { $notificationList->setPageNumber($page); } if ($compact && $page > 1) { $this->document->addFrom($notificationList); } else { $this->addContent($notificationList); } if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } } public function completeAction() { $suggestions = new ObjectSuggestions(); $suggestions->setModel(NotificationHistory::class); $suggestions->forRequest(ServerRequest::fromGlobals()); $this->getDocument()->add($suggestions); } public function searchEditorAction() { $editor = $this->createSearchEditor(NotificationHistory::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } } icingadb-web-1.4.0/application/controllers/RedundancygroupController.php000066400000000000000000000312571516124654200266440ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use Icinga\Module\Icingadb\Common\CommandActions; use Icinga\Module\Icingadb\Data\DependencyNodes; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\RedundancyGroup; use Icinga\Module\Icingadb\Model\RedundancyGroupSummary; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions; use Icinga\Module\Icingadb\Widget\Detail\RedundancyGroupDetail; use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Generator; use ipl\Orm\Query; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; use ipl\Web\Widget\Tabs; class RedundancygroupController extends Controller { use CommandActions; /** @var string */ protected $groupId; /** @var RedundancyGroup */ protected $group; /** @var RedundancyGroupSummary */ protected $groupSummary; public function init(): void { // in case of quick actions, param id is not given $groupId = $this->params->shift('child.redundancy_group.id'); if ($groupId === null) { $groupId = $this->params->shiftRequired('id'); } $this->groupId = $groupId; } /** * Load the redundancy group */ protected function loadGroup(): void { $query = DependencyNode::on($this->getDb()) ->with(['redundancy_group', 'redundancy_group.state']) ->filter(Filter::equal('redundancy_group_id', $this->groupId)); $this->applyRestrictions($query); $this->group = $query->first()?->redundancy_group; if ($this->group === null) { $this->httpNotFound($this->translate('Redundancy Group not found')); } $this->setTitleTab($this->getRequest()->getActionName()); $this->setTitle($this->group->display_name); $this->groupSummary = $this->group->summary; $this->addControl(new ObjectHeader($this->group)); } public function indexAction(): void { $this->loadGroup(); // The base filter is required to fetch the correct objects for MultiselectQuickActions::isGrantedOnType() check $this->addControl( (new MultiselectQuickActions('dependency_node', $this->groupSummary)) ->setBaseFilter(Filter::equal('child.redundancy_group.id', $this->groupId)) ->setAllowToProcessCheckResults(false) ->setColumnPrefix('nodes') ->setUrlPath('icingadb/redundancygroup') ); $this->addContent(new RedundancyGroupDetail($this->group)); } public function membersAction(): Generator { $this->loadGroup(); $nodesQuery = $this->fetchDependencyNodes(true); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($nodesQuery); $sortControl = $this->createSortControl( $nodesQuery, [ 'name' => $this->translate('Name'), 'severity desc, last_state_change desc' => $this->translate('Severity'), 'state' => $this->translate('Current State'), 'last_state_change desc' => $this->translate('Last State Change') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $searchBar = $this->createSearchBar( $nodesQuery, [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'id' ] ); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $nodesQuery->filter($filter); yield $this->export($nodesQuery); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $this->addContent( (new ObjectList($nodesQuery)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()) ); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } $this->setAutorefreshInterval(10); } public function childrenAction(): Generator { $this->loadGroup(); $nodesQuery = $this->fetchDependencyNodes(); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($nodesQuery); $sortControl = $this->createSortControl( $nodesQuery, [ 'name' => $this->translate('Name'), 'severity desc, last_state_change desc' => $this->translate('Severity'), 'state' => $this->translate('Current State'), 'last_state_change desc' => $this->translate('Last State Change') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $preserveParams = [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'id' ]; $requestParams = Url::fromRequest()->onlyWith($preserveParams)->getParams(); $searchBar = $this->createSearchBar($nodesQuery, $preserveParams) ->setEditorUrl( Url::fromPath('icingadb/redundancygroup/children-search-editor') ->setParams($requestParams) )->setSuggestionUrl( Url::fromPath('icingadb/redundancygroup/children-complete') ->setParams(clone $requestParams) ); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $nodesQuery->filter($filter); yield $this->export($nodesQuery); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $this->addContent( (new ObjectList($nodesQuery)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()) ); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } $this->setAutorefreshInterval(10); } public function completeAction(): void { $suggestions = (new ObjectSuggestions()) ->setModel(DependencyNode::class) ->onlyWithCustomVarSources(['host', 'service', 'hostgroup', 'servicegroup']) ->setBaseFilter(Filter::equal("child.redundancy_group.id", $this->groupId)) ->forRequest($this->getServerRequest()); $this->getDocument()->add($suggestions); } public function childrenCompleteAction(): void { $suggestions = (new ObjectSuggestions()) ->setModel(DependencyNode::class) ->onlyWithCustomVarSources(['host', 'service', 'hostgroup', 'servicegroup']) ->setBaseFilter(Filter::equal("parent.redundancy_group.id", $this->groupId)) ->forRequest($this->getServerRequest()); $this->getDocument()->add($suggestions); } public function searchEditorAction(): void { $editor = $this->createSearchEditor( DependencyNode::on($this->getDb()), Url::fromPath('icingadb/redundancygroup/members', ['id' => $this->groupId]), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'id' ] ); $this->getDocument()->add($editor); $this->setTitle($this->translate('Adjust Filter')); } public function childrenSearchEditorAction(): void { $preserveParams = [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'id' ]; $editor = $this->createSearchEditor( DependencyNode::on($this->getDb()), Url::fromPath('icingadb/redundancygroup/children', ['id' => $this->groupId]), $preserveParams ); $editor->setSuggestionUrl( Url::fromPath('icingadb/redundancygroup/children-complete') ->setParams(Url::fromRequest()->onlyWith($preserveParams)->getParams()) ); $this->getDocument()->add($editor); $this->setTitle($this->translate('Adjust Filter')); } protected function createTabs(): Tabs { $tabs = $this->getTabs() ->add('index', [ 'label' => $this->translate('Redundancy Group'), 'url' => Url::fromPath('icingadb/redundancygroup', ['id' => $this->groupId]) ]) ->add('members', [ 'label' => $this->translate('Members'), 'url' => Url::fromPath('icingadb/redundancygroup/members', ['id' => $this->groupId]) ]) ->add('children', [ 'label' => $this->translate('Children'), 'url' => Url::fromPath('icingadb/redundancygroup/children', ['id' => $this->groupId]) ]); return $tabs; } protected function setTitleTab(string $name): void { $tab = $this->createTabs()->get($name); if ($tab !== null) { $this->getTabs()->activate($name); } } /** * Fetch the dependency nodes of the current group * * @param bool $parents Whether to fetch the parents or the children * * @return Query */ private function fetchDependencyNodes(bool $parents = false): Query { $filterColumn = sprintf( '%s.redundancy_group.id', $parents ? 'child' : 'parent' ); $query = DependencyNode::on($this->getDb()) ->with([ 'host', 'host.state', 'host.state.last_comment', 'service', 'service.state', 'service.state.last_comment', 'service.host', 'service.host.state' ]) ->filter(Filter::equal($filterColumn, $this->groupId)); $this->applyRestrictions($query); return $query; } protected function fetchCommandTargets() { $filter = Filter::all(Filter::equal('child.redundancy_group.id', $this->groupId)); if ($this->getRequest()->getActionName() === 'acknowledge') { $filter->add( Filter::any( Filter::all( Filter::unlike('child.service.id', '*'), Filter::equal('host.state.is_problem', 'y'), Filter::equal('host.state.is_acknowledged', 'n') ), Filter::all( Filter::equal('service.state.is_problem', 'y'), Filter::equal('service.state.is_acknowledged', 'n') ) ) ); } return new DependencyNodes($filter); } protected function getCommandTargetsUrl(): Url { return Url::fromPath('icingadb/redundancygroup', ['id' => $this->groupId]); } public function processCheckresultAction(): void { $this->httpBadRequest('Check result submission not implemented yet'); } } icingadb-web-1.4.0/application/controllers/ServiceController.php000066400000000000000000000471001516124654200250650ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use ArrayIterator; use Generator; use Icinga\Exception\NotFoundError; use Icinga\Module\Icingadb\Command\Object\GetObjectCommand; use Icinga\Module\Icingadb\Command\Transport\CommandTransport; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Common\CommandActions; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\ServiceLinks; use Icinga\Module\Icingadb\Hook\TabHook\HookActions; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Util\OptimizerHints; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader; use Icinga\Module\Icingadb\Widget\Detail\QuickActions; use Icinga\Module\Icingadb\Widget\Detail\ServiceDetail; use Icinga\Module\Icingadb\Widget\Detail\ServiceInspectionDetail; use Icinga\Module\Icingadb\Widget\Detail\ServiceMetaInfo; use Icinga\Module\Icingadb\Widget\ItemList\LoadMoreObjectList; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use ipl\Orm\Query; use ipl\Sql\Expression; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; use ipl\Web\Widget\Tabs; class ServiceController extends Controller { use CommandActions; use HookActions; /** @var Service The service object */ protected $service; public function init(): void { $name = $this->params->shiftRequired('name'); $hostName = $this->params->shiftRequired('host.name'); $query = Service::on($this->getDb()) ->with([ 'state', 'icon_image', 'host', 'host.state', 'timeperiod' ]); $query ->setResultSetClass(VolatileStateResults::class) ->filter(Filter::all( Filter::equal('service.name', $name), Filter::equal('host.name', $hostName) )); if (Backend::supportsDependencies()) { $query->withColumns(['has_problematic_parent']); } $this->applyRestrictions($query); /** @var Service $service */ $service = $query->first(); if ($service === null) { throw new NotFoundError($this->translate('Service not found')); } $this->service = $service; $this->loadTabsForObject($service); $this->addControl(new ObjectHeader($service)); $this->setTitleTab($this->getRequest()->getActionName()); $this->setTitle( $this->translate('%s on %s', ' on '), $service->display_name, $service->host->display_name ); } public function indexAction(): void { if ($this->service->state->is_overdue) { $this->controls->addAttributes(['class' => 'overdue']); } $this->addControl(new ServiceMetaInfo($this->service)); $this->addControl(new QuickActions($this->service)); $this->addContent(new ServiceDetail($this->service)); $this->setAutorefreshInterval(10); } public function parentsAction(): Generator { $nodesQuery = $this->fetchDependencyNodes(true); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($nodesQuery); $sortControl = $this->createSortControl( $nodesQuery, [ 'name' => $this->translate('Name'), 'severity desc, last_state_change desc' => $this->translate('Severity'), 'state' => $this->translate('Current State'), 'last_state_change desc' => $this->translate('Last State Change') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $searchBar = $this->createSearchBar( $nodesQuery, [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'name', 'host.name' ] ); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $nodesQuery->filter($filter); yield $this->export($nodesQuery); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $this->addContent( (new ObjectList($nodesQuery)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()) ); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } $this->setAutorefreshInterval(10); } public function childrenAction(): Generator { $nodesQuery = $this->fetchDependencyNodes(); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($nodesQuery); $sortControl = $this->createSortControl( $nodesQuery, [ 'name' => $this->translate('Name'), 'severity desc, last_state_change desc' => $this->translate('Severity'), 'state' => $this->translate('Current State'), 'last_state_change desc' => $this->translate('Last State Change') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $preserveParams = [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'name', 'host.name' ]; $requestParams = Url::fromRequest()->onlyWith($preserveParams)->getParams(); $searchBar = $this->createSearchBar($nodesQuery, $preserveParams) ->setEditorUrl( Url::fromPath('icingadb/service/children-search-editor') ->setParams($requestParams) )->setSuggestionUrl( Url::fromPath('icingadb/service/children-complete') ->setParams(clone $requestParams) ); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $nodesQuery->filter($filter); yield $this->export($nodesQuery); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $this->addContent( (new ObjectList($nodesQuery)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()) ); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } $this->setAutorefreshInterval(10); } public function sourceAction(): void { $this->assertPermission('icingadb/object/show-source'); $apiResult = (new CommandTransport())->send( (new GetObjectCommand()) ->setObjects(new ArrayIterator([$this->service])) ); if ($this->service->state->is_overdue) { $this->controls->addAttributes(['class' => 'overdue']); } $this->addContent(new ServiceInspectionDetail( $this->service, reset($apiResult) )); } public function historyAction(): Generator { $compact = $this->view->compact; // TODO: Find a less-legacy way.. if ($this->service->state->is_overdue) { $this->controls->addAttributes(['class' => 'overdue']); } $db = $this->getDb(); $history = History::on($db)->with([ 'host', 'host.state', 'service', 'service.state', 'comment', 'downtime', 'flapping', 'notification', 'acknowledgement', 'state' ]); $history->filter(Filter::all( Filter::equal('history.host_id', $this->service->host_id), Filter::equal('history.service_id', $this->service->id) )); $before = $this->params->shift('before', time()); $url = Url::fromRequest()->setParams(clone $this->params); $url->setParams(['name' => $this->service->name, 'host.name' => $this->service->host->name]); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($history); $sortControl = $this->createSortControl( $history, [ 'history.event_time desc, history.event_type desc' => $this->translate('Event Time') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl, true); $preserveParams = [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'name', 'host.name' ]; $requestParams = Url::fromRequest()->onlyWith($preserveParams)->getParams(); $searchBar = $this->createSearchBar($history, $preserveParams) ->setEditorUrl( Url::fromPath('icingadb/service/history-search-editor') ->setParams($requestParams) )->setSuggestionUrl( Url::fromPath('icingadb/service/history-complete') ->setParams(clone $requestParams) ); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $history->peekAhead(); $page = $paginationControl->getCurrentPageNumber(); if ($page > 1 && ! $compact) { $history->resetOffset(); $history->limit($page * $limitControl->getLimit()); } $history->filter(Filter::lessThanOrEqual('event_time', $before)); $this->filter($history, $filter); OptimizerHints::disableOptimizerForHistoryQueries($history); yield $this->export($history); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $historyList = (new LoadMoreObjectList($history->execute())) ->setViewMode($viewModeSwitcher->getViewMode()) ->setPageSize($limitControl->getLimit()) ->setLoadMoreUrl($url->setParam('before', $before)->setFilter($filter)); if ($compact) { $historyList->setPageNumber($page); } if ($compact && $page > 1) { $this->document->addFrom($historyList); } else { $this->addContent($historyList); } if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } } public function completeAction(): void { $suggestions = (new ObjectSuggestions()) ->setModel(DependencyNode::class) ->onlyWithCustomVarSources(['host', 'service', 'hostgroup', 'servicegroup']) ->setBaseFilter(Filter::equal("child.service.id", $this->service->id)) ->forRequest($this->getServerRequest()); $this->getDocument()->add($suggestions); } public function childrenCompleteAction(): void { $suggestions = (new ObjectSuggestions()) ->setModel(DependencyNode::class) ->onlyWithCustomVarSources(['host', 'service', 'hostgroup', 'servicegroup']) ->setBaseFilter(Filter::equal("parent.service.id", $this->service->id)) ->forRequest($this->getServerRequest()); $this->getDocument()->add($suggestions); } public function historyCompleteAction(): void { $suggestions = (new ObjectSuggestions()) ->setModel(History::class) ->setBaseFilter(Filter::equal('service.id', $this->service->id)) ->forRequest($this->getServerRequest()); $this->getDocument()->addHtml($suggestions); } public function searchEditorAction(): void { $editor = $this->createSearchEditor( DependencyNode::on($this->getDb()), Url::fromPath( 'icingadb/service/parents', ['name' => $this->service->name, 'host.name' => $this->service->host->name] ), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'name', 'host.name' ] ); $this->getDocument()->add($editor); $this->setTitle($this->translate('Adjust Filter')); } public function childrenSearchEditorAction(): void { $preserveParams = [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'name', 'host.name' ]; $editor = $this->createSearchEditor( DependencyNode::on($this->getDb()), Url::fromPath( 'icingadb/service/children', ['name' => $this->service->name, 'host.name' => $this->service->host->name] ), $preserveParams ); $editor->setSuggestionUrl( Url::fromPath('icingadb/service/children-complete') ->setParams(Url::fromRequest()->onlyWith($preserveParams)->getParams()) ); $this->getDocument()->add($editor); $this->setTitle($this->translate('Adjust Filter')); } public function historySearchEditorAction(): void { $preserveParams = [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'name', 'host.name' ]; $editor = $this->createSearchEditor( History::on($this->getDb()), Url::fromPath('icingadb/service/history', ['name' => $this->service->name]), $preserveParams ); $editor->setSuggestionUrl( Url::fromPath('icingadb/service/history-complete') ->setParams(Url::fromRequest()->onlyWith($preserveParams)->getParams()) ); $this->getDocument()->addHtml($editor); $this->setTitle($this->translate('Adjust Filter')); } /** * Fetch the dependency nodes of the current service * * @param bool $parents Whether to fetch the parents or the children * * @return Query */ protected function fetchDependencyNodes(bool $parents = false): Query { $query = DependencyNode::on($this->getDb()) ->with([ 'host', 'host.state', 'host.state.last_comment', 'service', 'service.state', 'service.state.last_comment', 'service.host', 'service.host.state', 'redundancy_group', 'redundancy_group.state' ]) ->filter(Filter::equal( sprintf('%s.service.id', $parents ? 'child' : 'parent'), $this->service->id )) ->setResultSetClass(VolatileStateResults::class); $this->applyRestrictions($query); return $query; } protected function createTabs(): Tabs { if (! Backend::supportsDependencies()) { $hasDependencyNode = false; } else { $hasDependencyNode = DependencyNode::on($this->getDb()) ->columns([new Expression('1')]) ->filter(Filter::all( Filter::equal('service_id', $this->service->id), Filter::equal('host_id', $this->service->host_id) )) ->disableDefaultSort() ->first() !== null; } $tabs = $this->getTabs() ->add('index', [ 'label' => $this->translate('Service'), 'url' => Links::service($this->service, $this->service->host) ]) ->add('history', [ 'label' => $this->translate('History'), 'url' => ServiceLinks::history($this->service, $this->service->host) ]); if ($hasDependencyNode) { $tabs->add('parents', [ 'label' => $this->translate('Parents'), 'url' => Url::fromPath( 'icingadb/service/parents', ['name' => $this->service->name, 'host.name' => $this->service->host->name] ) ])->add('children', [ 'label' => $this->translate('Children'), 'url' => Url::fromPath( 'icingadb/service/children', ['name' => $this->service->name, 'host.name' => $this->service->host->name] ) ]); } if ($this->hasPermission('icingadb/object/show-source')) { $tabs->add('source', [ 'label' => $this->translate('Source'), 'url' => Links::serviceSource($this->service, $this->service->host) ]); } foreach ($this->loadAdditionalTabs() as $name => $tab) { $tabs->add($name, $tab + ['urlParams' => [ 'name' => $this->service->name, 'host.name' => $this->service->host->name ]]); } return $tabs; } protected function setTitleTab(string $name): void { $tab = $this->createTabs()->get($name); if ($tab !== null) { $this->getTabs()->activate($name); } } protected function fetchCommandTargets(): array { return [$this->service]; } protected function getCommandTargetsUrl(): Url { return Links::service($this->service, $this->service->host); } protected function getDefaultTabControls(): array { return [new ObjectHeader($this->service)]; } } icingadb-web-1.4.0/application/controllers/ServicegroupController.php000066400000000000000000000134521516124654200261450ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use Generator; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\ServicegroupSummary; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\ObjectHeader; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use ipl\Html\Html; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; class ServicegroupController extends Controller { /** @var string */ protected $servicegroupName; public function init() { $this->assertRouteAccess('servicegroups'); $this->servicegroupName = $this->params->shiftRequired('name'); } /** * Fetch the service group object * * @return ServicegroupSummary */ protected function fetchServicegroup(): ServicegroupSummary { $query = ServicegroupSummary::on($this->getDb()); foreach ($query->getUnions() as $unionPart) { $unionPart->filter(Filter::equal('servicegroup.name', $this->servicegroupName)); } $this->applyRestrictions($query); /** @var ServicegroupSummary $servicegroup */ $servicegroup = $query->first(); if ($servicegroup === null) { $this->httpNotFound(t('Service group not found')); } return $servicegroup; } public function indexAction(): Generator { $db = $this->getDb(); $servicegroup = $this->fetchServicegroup(); $services = Service::on($db)->with([ 'state', 'state.last_comment', 'icon_image', 'host', 'host.state' ]); $services ->setResultSetClass(VolatileStateResults::class) ->filter(Filter::equal('servicegroup.id', $servicegroup->id)); $this->applyRestrictions($services); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($services); $sortControl = $this->createSortControl( $services, [ 'service.display_name' => t('Name'), 'service.state.severity desc,service.state.last_state_change desc' => t('Severity'), 'service.state.soft_state' => t('Current State'), 'service.state.last_state_change desc' => t('Last State Change'), 'host.display_name' => t('Host') ] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $searchBar = $this->createSearchBar($services, [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'name' ]); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $services->filter($filter); yield $this->export($services); $results = $services->execute(); $serviceList = (new ObjectList($results)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()); // ICINGAWEB_EXPORT_FORMAT is not set yet and $this->format is inaccessible, yeah... if ($this->getRequest()->getParam('format') === 'pdf') { $this->addContent(new ObjectHeader($servicegroup)); $this->addContent(Html::tag('h2', null, t('Services'))); } else { $this->addControl(new ObjectHeader($servicegroup)); } $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $continueWith = $this->createContinueWith( Links::servicesDetails() ->setFilter(Filter::equal('servicegroup.name', $servicegroup->name)), $searchBar, $results->hasResult() ); $this->addContent($serviceList); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate($continueWith); } $this->addTitleTab(t('Service Group')); $this->setTitle($servicegroup->display_name); $this->setAutorefreshInterval(10); } public function completeAction(): void { $suggestions = new ObjectSuggestions(); $suggestions->setModel(Service::class); $suggestions->setBaseFilter(Filter::equal('servicegroup.name', $this->servicegroupName)); $suggestions->forRequest($this->getServerRequest()); $this->getDocument()->add($suggestions); } public function searchEditorAction(): void { $editor = $this->createSearchEditor(Service::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'name' ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } } icingadb-web-1.4.0/application/controllers/ServicegroupsController.php000066400000000000000000000107551516124654200263330ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Model\Servicegroup; use Icinga\Module\Icingadb\Model\ServicegroupSummary; use Icinga\Module\Icingadb\View\ServicegroupGridRenderer; use Icinga\Module\Icingadb\View\ServicegroupRenderer; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Widget\ItemTable\ObjectGrid; use Icinga\Module\Icingadb\Widget\ItemTable\ObjectTable; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Html\Attributes; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; use ipl\Web\Widget\ItemList; class ServicegroupsController extends Controller { public function init() { parent::init(); $this->assertRouteAccess(); } public function indexAction() { $this->addTitleTab(t('Service Groups')); $compact = $this->view->compact; $db = $this->getDb(); $servicegroups = ServicegroupSummary::on($db); $this->handleSearchRequest($servicegroups); $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($servicegroups); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $sortControl = $this->createSortControl( $servicegroups, [ 'display_name' => t('Name'), 'services_severity desc, display_name' => t('Severity'), 'services_total desc' => t('Total Services') ], ['services_severity DESC', 'display_name'] ); $searchBar = $this->createSearchBar($servicegroups, [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam() ]); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $this->filter($servicegroups, $filter); $servicegroups->peekAhead($compact); yield $this->export($servicegroups); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $results = $servicegroups->execute(); if ($viewModeSwitcher->getViewMode() === 'grid') { $content = new ObjectGrid($results, (new ServicegroupGridRenderer())->setBaseFilter($filter)); } else { $content = new ObjectTable($results, (new ServicegroupRenderer())->setBaseFilter($filter)); } $content->setEmptyStateMessage($paginationControl->getEmptyStateMessage()); $this->addContent($content); if ($compact) { $this->addContent( (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view']))) ->setBaseTarget('_next') ->setAttribute('title', sprintf( t('Show all %d servicegroups'), $servicegroups->count() )) ); } if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } $this->setAutorefreshInterval(30); } public function completeAction() { $suggestions = new ObjectSuggestions(); $suggestions->setModel(Servicegroup::class); $suggestions->forRequest(ServerRequest::fromGlobals()); $this->getDocument()->add($suggestions); } public function searchEditorAction() { $editor = $this->createSearchEditor(ServicegroupSummary::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } } icingadb-web-1.4.0/application/controllers/ServicesController.php000066400000000000000000000366571516124654200252670ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Common\CommandActions; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Data\PivotTable; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\ServicestateSummary; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Util\FeatureStatus; use Icinga\Module\Icingadb\Web\Control\ProblemToggle; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\Detail\MultiselectQuickActions; use Icinga\Module\Icingadb\Widget\Detail\ObjectsDetail; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Widget\ItemTable\ServiceItemTable; use Icinga\Module\Icingadb\Widget\ServiceStatusBar; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Widget\ShowMore; use Icinga\Util\Environment; use ipl\Html\HtmlString; use ipl\Orm\Query; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; use ipl\Web\Url; class ServicesController extends Controller { use CommandActions; public function indexAction() { $this->addTitleTab(t('Services')); $compact = $this->view->compact; $db = $this->getDb(); $services = Service::on($db)->with([ 'state', 'state.last_comment', 'host', 'host.state', 'icon_image' ]); $services->getWith()['service.state']->setJoinType('INNER'); $services->setResultSetClass(VolatileStateResults::class); $this->handleSearchRequest($services); $summary = null; if (! $compact) { $summary = ServicestateSummary::on($db); } $limitControl = $this->createLimitControl(); $paginationControl = $this->createPaginationControl($services); $sortControl = $this->createSortControl( $services, [ 'service.display_name' => t('Name'), 'service.state.severity desc,service.state.last_state_change desc' => t('Severity'), 'service.state.soft_state' => t('Current State'), 'service.state.last_state_change desc' => t('Last State Change'), 'host.display_name' => t('Host') ], ['service.state.severity DESC', 'service.state.last_state_change DESC'] ); $viewModeSwitcher = $this->createViewModeSwitcher($paginationControl, $limitControl); $columns = $this->createColumnControl($services, $viewModeSwitcher); $searchBar = $this->createSearchBar($services, [ $limitControl->getLimitParam(), $sortControl->getSortParam(), $viewModeSwitcher->getViewModeParam(), 'columns' ]); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $services->peekAhead($compact); $this->filter($services, $filter); if (! $compact) { $this->filter($summary, $filter); yield $this->export($services, $summary); } else { yield $this->export($services); } $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addControl($searchBar); $results = $services->execute(); $continueWith = $this->createContinueWith(Links::servicesDetails(), $searchBar, $results->hasResult()); if ($viewModeSwitcher->getViewMode() === 'tabular') { $serviceList = (new ServiceItemTable($results, ServiceItemTable::applyColumnMetaData($services, $columns))) ->setSort($sortControl->getSort()); } else { $serviceList = (new ObjectList($results)) ->setViewMode($viewModeSwitcher->getViewMode()); } $serviceList->setEmptyStateMessage($paginationControl->getEmptyStateMessage()); $this->addContent($serviceList); if ($compact) { $this->addContent( (new ShowMore($results, Url::fromRequest()->without(['showCompact', 'limit', 'view']))) ->setBaseTarget('_next') ->setAttribute('title', sprintf( t('Show all %d services'), $services->count() )) ); } else { /** @var ServicestateSummary $servicesSummary */ $servicesSummary = $summary->first(); $this->addFooter((new ServiceStatusBar($servicesSummary))->setBaseFilter($filter)); } if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate($continueWith); } $this->setAutorefreshInterval(10); } public function detailsAction() { $this->addTitleTab(t('Services')); $db = $this->getDb(); $services = Service::on($db)->with([ 'state', 'icon_image', 'host', 'host.state' ]); $services->setResultSetClass(VolatileStateResults::class); $summary = ServicestateSummary::on($db)->with(['state']); $this->filter($services); $this->filter($summary); $services->limit(3); $services->peekAhead(); yield $this->export($services, $summary); $results = $services->execute(); $summary = $summary->first(); $downtimes = Service::on($db)->with(['downtime']); $downtimes->getWith()['service.downtime']->setJoinType('INNER'); $this->filter($downtimes); $summary->downtimes_total = $downtimes->count(); $comments = Service::on($db)->with(['comment']); $comments->getWith()['service.comment']->setJoinType('INNER'); // TODO: This should be automatically done by the model/resolver and added as ON condition $comments->filter(Filter::equal('comment.object_type', 'service')); $this->filter($comments); $summary->comments_total = $comments->count(); $this->addControl( (new ObjectList($results)) ->setViewMode('minimal') ->setDetailActionsDisabled() ); $this->addControl(new ShowMore( $results, Links::services()->setFilter($this->getFilter()), sprintf(t('Show all %d services'), $services->count()) )); $this->addControl( (new MultiselectQuickActions('service', $summary)) ->setBaseFilter($this->getFilter()) ); $this->addContent( (new ObjectsDetail('service', $summary, $services)) ->setBaseFilter($this->getFilter()) ); } public function completeAction() { $suggestions = new ObjectSuggestions(); $suggestions->setModel(Service::class); $suggestions->forRequest(ServerRequest::fromGlobals()); $this->getDocument()->add($suggestions); } public function searchEditorAction() { $editor = $this->createSearchEditor(Service::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM, 'columns' ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } public function gridAction() { Environment::raiseExecutionTime(); $db = $this->getDb(); $this->addTitleTab(t('Service Grid')); $query = Service::on($db)->with([ 'state', 'host', 'host.state' ]); $query->setResultSetClass(VolatileStateResults::class); $this->handleSearchRequest($query); // Create problem toggle before shifting params. Otherwise, the toggle // will redirect to the grid without the shifted params. $problemToggle = $this->createProblemToggle(); $this->params->shift('page'); // Handled by PivotTable internally $this->params->shift('limit'); // Handled by PivotTable internally $flipped = $this->params->shift('flipped', false); $sortControl = $this->createSortControl($query, [ 'service.display_name' => t('Service Name'), 'host.display_name' => t('Host Name'), ])->setDefault('service.display_name'); $searchBar = $this->createSearchBar($query, [ LimitControl::DEFAULT_LIMIT_PARAM, $sortControl->getSortParam(), 'flipped', 'page', 'problems' ]); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $this->filter($query, $filter); $this->addControl($problemToggle); $this->addControl($sortControl); $this->addControl($searchBar); $pivotFilter = $problemToggle->isChecked() ? Filter::equal('service.state.is_problem', 'y') : null; $columns = [ 'id', 'host.id', 'host_name' => 'host.name', 'host_display_name' => 'host.display_name', 'name' => 'service.name', 'display_name' => 'service.display_name', 'service.state.is_handled', 'service.state.output', 'service.state.soft_state' ]; if ($flipped) { $pivot = (new PivotTable($query, 'host_name', 'name', $columns)) ->setXAxisFilter($pivotFilter) ->setYAxisFilter($pivotFilter ? clone $pivotFilter : null) ->setXAxisHeader('host_display_name') ->setYAxisHeader('display_name'); } else { $pivot = (new PivotTable($query, 'name', 'host_name', $columns)) ->setXAxisFilter($pivotFilter) ->setYAxisFilter($pivotFilter ? clone $pivotFilter : null) ->setXAxisHeader('display_name') ->setYAxisHeader('host_display_name'); } $this->view->horizontalPaginator = $pivot->paginateXAxis(); $this->view->verticalPaginator = $pivot->paginateYAxis(); list($pivotData, $pivotHeader) = $pivot->toArray(); $this->view->pivotData = $pivotData; $this->view->pivotHeader = $pivotHeader; $continueWith = $this->createContinueWith(Links::servicesDetails(), $searchBar, ! empty($pivotData)); /** Preserve filter and params in view links (the `BaseFilter` implementation for view scripts -.-) */ $this->view->baseUrl = Url::fromRequest() ->onlyWith([ LimitControl::DEFAULT_LIMIT_PARAM, $sortControl->getSortParam(), 'flipped', 'page', 'problems' ]); $preservedParams = $this->view->baseUrl->getParams(); $this->view->baseUrl->setFilter($filter); $searchBar->setEditorUrl(Url::fromPath( "icingadb/services/grid-search-editor" )->setParams($preservedParams)); $this->view->controls = $this->controls; if ($flipped) { $this->getHelper('viewRenderer')->setScriptAction('grid-flipped'); } if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { // TODO: Everything up to addContent() (inclusive) can be removed once the grid is a widget $this->view->controls = ''; // Relevant controls are transmitted separately $viewRenderer = $this->getHelper('viewRenderer'); $viewRenderer->postDispatch(); $viewRenderer->setNoRender(false); $content = trim($this->getResponse()); $this->getResponse()->clearBody($viewRenderer->getResponseSegment()); $this->addContent(HtmlString::create(substr($content, strpos($content, '>') + 1, -6))); $this->sendMultipartUpdate($continueWith); } $this->setAutorefreshInterval(30); } public function gridSearchEditorAction() { $editor = $this->createSearchEditor( Service::on($this->getDb()), Url::fromPath('icingadb/services/grid'), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, 'flipped', 'page', 'problems' ] ); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } protected function fetchCommandTargets(): Query { $db = $this->getDb(); $services = Service::on($db)->with([ 'state', 'host', 'host.state' ]); $services->setResultSetClass(VolatileStateResults::class); switch ($this->getRequest()->getActionName()) { case 'acknowledge': $services->filter(Filter::equal('state.is_problem', 'y')) ->filter(Filter::equal('state.is_acknowledged', 'n')); break; } $this->filter($services); return $services; } protected function getCommandTargetsUrl(): Url { return Links::servicesDetails()->setFilter($this->getFilter()); } protected function getFeatureStatus() { $summary = ServicestateSummary::on($this->getDb()); $this->filter($summary); return new FeatureStatus('service', $summary->first()); } protected function prepareSearchFilter(Query $query, string $search, Filter\Any $filter, array $additionalColumns) { if ($this->params->shift('_hostFilterOnly', false)) { foreach (['host.name_ci', 'host.display_name', 'host.address', 'host.address6'] as $column) { $filter->add(Filter::like($column, "*$search*")); } } else { parent::prepareSearchFilter($query, $search, $filter, $additionalColumns); } } public function createProblemToggle(): ProblemToggle { $filter = $this->params->shift('problems'); $problemToggle = new ProblemToggle($filter); $problemToggle->setIdProtector([$this->getRequest(), 'protectId']); $problemToggle->on(ProblemToggle::ON_SUCCESS, function (ProblemToggle $form) { if (! $form->getElement('problems')->isChecked()) { $this->redirectNow(Url::fromRequest()->remove('problems')); } else { $this->redirectNow(Url::fromRequest()->setParams($this->params->add('problems'))); } })->handleRequest(ServerRequest::fromGlobals()); return $problemToggle; } } icingadb-web-1.4.0/application/controllers/SuggestController.php000066400000000000000000000032301516124654200251020ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\ProvidedHook\Notifications\V1\Source; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use ipl\Web\Compat\CompatController; class SuggestController extends CompatController { public function restrictionColumnAction(): void { $suggestions = (new ObjectSuggestions()) ->setModel( match ($this->params->getRequired('type')) { 'host', Source::TYPE_ALL => Host::class, 'service' => Service::class, default => $this->httpBadRequest('Invalid type') } ) ->onlyWithCustomVarSources(['host', 'service']) ->withFixedColumns([ 'host.name' => $this->translate('Host Name'), 'hostgroup.name' => $this->translate('Hostgroup Name'), 'host.user.name' => $this->translate('Contact Name'), 'host.usergroup.name' => $this->translate('Contactgroup Name'), 'service.name' => $this->translate('Service Name'), 'servicegroup.name' => $this->translate('Servicegroup Name'), 'service.user.name' => $this->translate('Contact Name'), 'service.usergroup.name' => $this->translate('Contactgroup Name') ]); $this->getDocument()->addHtml( $suggestions->forRequest($this->getServerRequest()) ); } } icingadb-web-1.4.0/application/controllers/TacticalController.php000066400000000000000000000056361516124654200252210ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Controllers; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Module\Icingadb\Model\HoststateSummary; use Icinga\Module\Icingadb\Model\ServicestateSummary; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use Icinga\Module\Icingadb\Web\Controller; use Icinga\Module\Icingadb\Widget\HostSummaryDonut; use Icinga\Module\Icingadb\Widget\ServiceSummaryDonut; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use ipl\Orm\Query; use ipl\Stdlib\Filter; use ipl\Web\Control\LimitControl; use ipl\Web\Control\SortControl; class TacticalController extends Controller { public function indexAction() { $this->addTitleTab(t('Tactical Overview')); $db = $this->getDb(); $hoststateSummary = HoststateSummary::on($db); $servicestateSummary = ServicestateSummary::on($db); $this->handleSearchRequest($servicestateSummary, [ 'host.name_ci', 'host.display_name', 'host.address', 'host.address6' ]); $searchBar = $this->createSearchBar($servicestateSummary); if ($searchBar->hasBeenSent() && ! $searchBar->isValid()) { if ($searchBar->hasBeenSubmitted()) { $filter = $this->getFilter(); } else { $this->addControl($searchBar); $this->sendMultipartUpdate(); return; } } else { $filter = $searchBar->getFilter(); } $this->filter($hoststateSummary, $filter); $this->filter($servicestateSummary, $filter); yield $this->export($hoststateSummary, $servicestateSummary); $this->addControl($searchBar); $this->addContent( (new HostSummaryDonut($hoststateSummary->first())) ->setBaseFilter($filter) ); $this->addContent( (new ServiceSummaryDonut($servicestateSummary->first())) ->setBaseFilter($filter) ); if (! $searchBar->hasBeenSubmitted() && $searchBar->hasBeenSent()) { $this->sendMultipartUpdate(); } $this->setAutorefreshInterval(10); } public function completeAction() { $suggestions = new ObjectSuggestions(); $suggestions->setModel(ServicestateSummary::class); $suggestions->forRequest(ServerRequest::fromGlobals()); $this->getDocument()->add($suggestions); } public function searchEditorAction() { $editor = $this->createSearchEditor(ServicestateSummary::on($this->getDb()), [ LimitControl::DEFAULT_LIMIT_PARAM, SortControl::DEFAULT_SORT_PARAM, ViewModeSwitcher::DEFAULT_VIEW_MODE_PARAM ]); $this->getDocument()->add($editor); $this->setTitle(t('Adjust Filter')); } } icingadb-web-1.4.0/application/forms/000077500000000000000000000000001516124654200174665ustar00rootroot00000000000000icingadb-web-1.4.0/application/forms/ApiTransportForm.php000066400000000000000000000061771516124654200234640ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms; use Icinga\Data\ConfigObject; use Icinga\Module\Icingadb\Command\Transport\CommandTransport; use Icinga\Module\Icingadb\Command\Transport\CommandTransportException; use Icinga\Web\Session; use ipl\Web\Common\CsrfCounterMeasure; use ipl\Web\Compat\CompatForm; class ApiTransportForm extends CompatForm { use CsrfCounterMeasure; protected function assemble() { // TODO: Use a validator to check if a name is not already in use $this->addElement('text', 'name', [ 'required' => true, 'label' => t('Transport Name') ]); $this->addElement('hidden', 'transport', [ 'value' => 'api' ]); $this->addElement('text', 'host', [ 'required' => true, 'id' => 'api_transport_host', 'label' => t('Host'), 'description' => t('Hostname or address of the Icinga master') ]); // TODO: Don't rely only on browser validation $this->addElement('number', 'port', [ 'required' => true, 'label' => t('Port'), 'value' => 5665, 'min' => 1, 'max' => 65536 ]); $this->addElement('text', 'username', [ 'required' => true, 'label' => t('API Username'), 'description' => t('User to authenticate with using HTTP Basic Auth') ]); $this->addElement('password', 'password', [ 'required' => true, 'autocomplete' => 'new-password', 'label' => t('API Password') ]); $this->addElement('submit', 'btn_submit', [ 'label' => t('Save') ]); $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId())); } public function validate() { parent::validate(); if (! $this->isValid) { return $this; } if ($this->getPopulatedValue('force_creation') === 'y') { return $this; } try { CommandTransport::createTransport(new ConfigObject($this->getValues()))->probe(); } catch (CommandTransportException $e) { $this->addMessage( sprintf(t('Failed to successfully validate the configuration: %s'), $e->getMessage()) ); $forceCheckbox = $this->createElement( 'checkbox', 'force_creation', [ 'ignore' => true, 'label' => t('Force Changes'), 'description' => t('Check this box to enforce changes without connectivity validation') ] ); $this->registerElement($forceCheckbox); $this->decorate($forceCheckbox); $this->prepend($forceCheckbox); $this->isValid = false; } return $this; } } icingadb-web-1.4.0/application/forms/Command/000077500000000000000000000000001516124654200210445ustar00rootroot00000000000000icingadb-web-1.4.0/application/forms/Command/CommandForm.php000066400000000000000000000077261516124654200237730ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command; use ArrayIterator; use Countable; use Exception; use Icinga\Application\Logger; use Icinga\Module\Icingadb\Command\IcingaCommand; use Icinga\Module\Icingadb\Command\Transport\CommandTransport; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Web\Notification; use Icinga\Web\Session; use ipl\Html\Form; use ipl\Orm\Model; use ipl\Web\Common\CsrfCounterMeasure; use Iterator; use IteratorIterator; use Traversable; abstract class CommandForm extends Form { use Auth; use CsrfCounterMeasure; protected $defaultAttributes = ['class' => 'icinga-form icinga-controls']; /** @var (Traversable&Countable)|array */ protected $objects; /** @var bool */ protected $isApiTarget = false; /** * Whether an error occurred while sending the command * * Prevents the success message from being rendered simultaneously * * @var bool */ protected $errorOccurred = false; /** * Set the objects to issue the command for * * @param (Traversable&Countable)|array $objects A traversable that is also countable * * @return $this */ public function setObjects($objects): self { $this->objects = $objects; return $this; } /** * Get the objects to issue the command for * * @return (Traversable&Countable)|array */ public function getObjects() { return $this->objects; } /** * Set whether this form is an API target * * @param bool $state * * @return $this */ public function setIsApiTarget(bool $state = true): self { $this->isApiTarget = $state; return $this; } /** * Get whether this form is an API target * * @return bool */ public function isApiTarget(): bool { return $this->isApiTarget; } /** * Create and add form elements representing the command's options * * @return void */ abstract protected function assembleElements(); /** * Create and add a submit button to the form * * @return void */ abstract protected function assembleSubmitButton(); /** * Get the commands to issue for the given objects * * @param Iterator $objects * * @return Traversable */ abstract protected function getCommands(Iterator $objects): Traversable; protected function assemble() { $this->assembleElements(); if (! $this->isApiTarget()) { $this->assembleSubmitButton(); $this->addElement($this->createCsrfCounterMeasure(Session::getSession()->getId())); } } protected function onSuccess() { $objects = $this->getObjects(); if (is_array($objects)) { $objects = new ArrayIterator($objects); } else { $objects = new IteratorIterator($objects); } $errors = []; foreach ($this->getCommands($objects) as $command) { try { $this->sendCommand($command); } catch (Exception $e) { Logger::error($e->getMessage()); $errors[] = $e->getMessage(); } } if (! empty($errors)) { if (count($errors) > 1) { Notification::warning( t('Some commands were not transmitted. Please check the log. The first error follows.') ); } $this->errorOccurred = true; Notification::error($errors[0]); } } /** * Transmit the given command * * @param IcingaCommand $command * * @return void */ protected function sendCommand(IcingaCommand $command) { (new CommandTransport())->send($command); } } icingadb-web-1.4.0/application/forms/Command/Instance/000077500000000000000000000000001516124654200226105ustar00rootroot00000000000000icingadb-web-1.4.0/application/forms/Command/Instance/ToggleInstanceFeaturesForm.php000066400000000000000000000130301516124654200305470ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Instance; use Icinga\Module\Icingadb\Command\Instance\ToggleInstanceFeatureCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Web\Notification; use ipl\Web\FormDecorator\IcingaFormDecorator; use Iterator; use Traversable; class ToggleInstanceFeaturesForm extends CommandForm { protected $features; protected $featureStatus; /** * ToggleFeature(s) being used to submit this form * * @var ToggleInstanceFeatureCommand[] */ protected $submittedFeatures = []; public function __construct(array $featureStatus) { $this->featureStatus = $featureStatus; $this->features = [ ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS => t('Active Host Checks'), ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS => t('Active Service Checks'), ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS => t('Event Handlers'), ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION => t('Flap Detection'), ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS => t('Notifications'), ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA => t('Performance Data') ]; $this->getAttributes()->add('class', 'instance-features'); $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } foreach ($this->submittedFeatures as $feature) { $enabled = $feature->getEnabled(); switch ($feature->getFeature()) { case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS: if ($enabled) { $message = t('Enabled active host checks successfully'); } else { $message = t('Disabled active host checks successfully'); } break; case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS: if ($enabled) { $message = t('Enabled active service checks successfully'); } else { $message = t('Disabled active service checks successfully'); } break; case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS: if ($enabled) { $message = t('Enabled event handlers successfully'); } else { $message = t('Disabled event handlers checks successfully'); } break; case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION: if ($enabled) { $message = t('Enabled flap detection successfully'); } else { $message = t('Disabled flap detection successfully'); } break; case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS: if ($enabled) { $message = t('Enabled notifications successfully'); } else { $message = t('Disabled notifications successfully'); } break; case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA: if ($enabled) { $message = t('Enabled performance data successfully'); } else { $message = t('Disabled performance data successfully'); } break; default: $message = t('Invalid feature option'); break; } Notification::success($message); } }); } protected function assembleElements() { $disabled = ! $this->getAuth()->hasPermission('icingadb/command/feature/instance'); $decorator = new IcingaFormDecorator(); foreach ($this->features as $feature => $label) { $this->addElement( 'checkbox', $feature, [ 'class' => 'autosubmit', 'label' => $label, 'disabled' => $disabled, 'value' => (bool) $this->featureStatus[$feature] ] ); $decorator->decorate($this->getElement($feature)); } } protected function assembleSubmitButton() { } protected function getCommands(Iterator $objects): Traversable { foreach ($this->features as $feature => $spec) { $featureState = $this->getElement($feature)->isChecked(); if ((int) $featureState === (int) $this->featureStatus[$feature]) { continue; } $command = new ToggleInstanceFeatureCommand(); $command->setFeature($feature); $command->setEnabled($featureState); $this->submittedFeatures[] = $command; yield $command; } } } icingadb-web-1.4.0/application/forms/Command/Object/000077500000000000000000000000001516124654200222525ustar00rootroot00000000000000icingadb-web-1.4.0/application/forms/Command/Object/AcknowledgeProblemForm.php000066400000000000000000000205641516124654200273620ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use CallbackFilterIterator; use DateInterval; use DateTime; use Icinga\Application\Config; use Icinga\Module\Icingadb\Command\Object\AcknowledgeProblemCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Module\Icingadb\Model\Host; use Icinga\Web\Notification; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Orm\Model; use ipl\Validator\CallbackValidator; use ipl\Web\FormDecorator\IcingaFormDecorator; use ipl\Web\Widget\Icon; use Iterator; use Traversable; use function ipl\Stdlib\iterable_value_first; class AcknowledgeProblemForm extends CommandForm { public function __construct() { $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } $countObjects = count($this->getObjects()); if (iterable_value_first($this->getObjects()) instanceof Host) { $message = sprintf(tp( 'Acknowledged problem successfully', 'Acknowledged problem on %d hosts successfully', $countObjects ), $countObjects); } else { $message = sprintf(tp( 'Acknowledged problem successfully', 'Acknowledged problem on %d services successfully', $countObjects ), $countObjects); } Notification::success($message); }); } protected function assembleElements() { $this->addHtml(new HtmlElement( 'div', Attributes::create(['class' => 'form-description']), new Icon('info-circle', ['class' => 'form-description-icon']), new HtmlElement( 'ul', null, new HtmlElement('li', null, Text::create(t( 'This command is used to acknowledge host or service problems. When a problem is acknowledged,' . ' future notifications about problems are temporarily disabled until the host or service' . ' recovers.' ))) ) )); $config = Config::module('icingadb'); $decorator = new IcingaFormDecorator(); $this->addElement( 'textarea', 'comment', [ 'required' => true, 'label' => t('Comment'), 'description' => t( 'If you work with other administrators, you may find it useful to share information about' . ' the host or service that is having problems. Make sure you enter a brief description of' . ' what you are doing.' ), 'value' => $config->get('settings', 'acknowledge_comment_text') ] ); $decorator->decorate($this->getElement('comment')); $this->addElement( 'checkbox', 'persistent', [ 'label' => t('Persistent Comment'), 'value' => (bool) $config->get('settings', 'acknowledge_persistent', false), 'description' => t( 'If you want the comment to remain even when the acknowledgement is removed, check this' . ' option.' ) ] ); $decorator->decorate($this->getElement('persistent')); $this->addElement( 'checkbox', 'notify', [ 'label' => t('Send Notification'), 'value' => (bool) $config->get('settings', 'acknowledge_notify', true), 'description' => t( 'If you want an acknowledgement notification to be sent out to the appropriate contacts,' . ' check this option.' ) ] ); $decorator->decorate($this->getElement('notify')); $this->addElement( 'checkbox', 'sticky', [ 'label' => t('Sticky Acknowledgement'), 'value' => (bool) $config->get('settings', 'acknowledge_sticky', false), 'description' => t( 'If you want the acknowledgement to remain until the host or service recovers even if the host' . ' or service changes state, check this option.' ) ] ); $decorator->decorate($this->getElement('sticky')); $this->addElement( 'checkbox', 'expire', [ 'ignore' => true, 'class' => 'autosubmit', 'value' => (bool) $config->get('settings', 'acknowledge_expire', false), 'label' => t('Use Expire Time'), 'description' => t('If the acknowledgement should expire, check this option.') ] ); $decorator->decorate($this->getElement('expire')); if ($this->getElement('expire')->isChecked()) { $expireTime = new DateTime(); $expireTime->add(new DateInterval($config->get('settings', 'acknowledge_expire_time', 'PT1H'))); $this->addElement( 'localDateTime', 'expire_time', [ 'data-use-datetime-picker' => true, 'required' => true, 'value' => $expireTime, 'label' => t('Expire Time'), 'description' => t( 'Choose the date and time when Icinga should delete the acknowledgement.' ), 'validators' => [ 'DateTime' => ['break_chain_on_failure' => true], 'Callback' => function ($value, $validator) { /** @var CallbackValidator $validator */ if ($value <= (new DateTime())) { $validator->addMessage(t('The expire time must not be in the past')); return false; } return true; } ] ] ); $decorator->decorate($this->getElement('expire_time')); } } protected function assembleSubmitButton() { $this->addElement( 'submit', 'btn_submit', [ 'required' => true, 'label' => tp( 'Acknowledge problem', 'Acknowledge problems', count($this->getObjects()) ), 'data-progress-label' => tp( 'Acknowledging problem', 'Acknowledging problems', count($this->getObjects()) ) ] ); (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); } protected function getCommands(Iterator $objects): Traversable { $granted = new CallbackFilterIterator($objects, function (Model $object): bool { return $this->isGrantedOn('icingadb/command/acknowledge-problem', $object); }); $command = new AcknowledgeProblemCommand(); $command->setComment($this->getValue('comment')); $command->setAuthor($this->getAuth()->getUser()->getUsername()); $command->setNotify($this->getElement('notify')->isChecked()); $command->setSticky($this->getElement('sticky')->isChecked()); $command->setPersistent($this->getElement('persistent')->isChecked()); if (($expireTime = $this->getValue('expire_time')) !== null) { /** @var DateTime $expireTime */ $command->setExpireTime($expireTime->getTimestamp()); } $granted->rewind(); // Forwards the pointer to the first element if ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects($granted)->setChunkSize(250); } } } icingadb-web-1.4.0/application/forms/Command/Object/AddCommentForm.php000066400000000000000000000137141516124654200256300ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use CallbackFilterIterator; use DateInterval; use DateTime; use Icinga\Application\Config; use Icinga\Module\Icingadb\Command\Object\AddCommentCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Module\Icingadb\Model\Host; use Icinga\Web\Notification; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Orm\Model; use ipl\Validator\CallbackValidator; use ipl\Web\FormDecorator\IcingaFormDecorator; use ipl\Web\Widget\Icon; use Iterator; use Traversable; use function ipl\Stdlib\iterable_value_first; class AddCommentForm extends CommandForm { public function __construct() { $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } $countObjects = count($this->getObjects()); if (iterable_value_first($this->getObjects()) instanceof Host) { $message = sprintf( tp('Added comment successfully', 'Added comment to %d hosts successfully', $countObjects), $countObjects ); } else { $message = sprintf( tp('Added comment successfully', 'Added comment to %d services successfully', $countObjects), $countObjects ); } Notification::success($message); }); } protected function assembleElements() { $this->addHtml(new HtmlElement( 'div', Attributes::create(['class' => 'form-description']), new Icon('info-circle', ['class' => 'form-description-icon']), new HtmlElement( 'ul', null, new HtmlElement( 'li', null, Text::create(t('This command is used to add host or service comments.')) ) ) )); $decorator = new IcingaFormDecorator(); $this->addElement( 'textarea', 'comment', [ 'required' => true, 'label' => t('Comment'), 'description' => t( 'If you work with other administrators, you may find it useful to share information about' . ' the host or service that is having problems. Make sure you enter a brief description of' . ' what you are doing.' ) ] ); $decorator->decorate($this->getElement('comment')); $config = Config::module('icingadb'); $this->addElement( 'checkbox', 'expire', [ 'ignore' => true, 'class' => 'autosubmit', 'value' => (bool) $config->get('settings', 'comment_expire', false), 'label' => t('Use Expire Time'), 'description' => t('If the comment should expire, check this option.') ] ); $decorator->decorate($this->getElement('expire')); if ($this->getElement('expire')->isChecked()) { $expireTime = new DateTime(); $expireTime->add(new DateInterval($config->get('settings', 'comment_expire_time', 'PT1H'))); $this->addElement( 'localDateTime', 'expire_time', [ 'data-use-datetime-picker' => true, 'required' => true, 'value' => $expireTime, 'label' => t('Expire Time'), 'description' => t('Choose the date and time when Icinga should delete the comment.'), 'validators' => [ 'DateTime' => ['break_chain_on_failure' => true], 'Callback' => function ($value, $validator) { /** @var CallbackValidator $validator */ if ($value <= (new DateTime())) { $validator->addMessage(t('The expire time must not be in the past')); return false; } return true; } ] ] ); $decorator->decorate($this->getElement('expire_time')); } } protected function assembleSubmitButton() { $this->addElement( 'submit', 'btn_submit', [ 'required' => true, 'label' => tp('Add comment', 'Add comments', count($this->getObjects())), 'data-progress-label' => tp('Adding comment', 'Adding comments', count($this->getObjects())) ] ); (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); } protected function getCommands(Iterator $objects): Traversable { $granted = new CallbackFilterIterator($objects, function (Model $object): bool { return $this->isGrantedOn('icingadb/command/comment/add', $object); }); $command = new AddCommentCommand(); $command->setComment($this->getValue('comment')); $command->setAuthor($this->getAuth()->getUser()->getUsername()); if (($expireTime = $this->getValue('expire_time'))) { /** @var DateTime $expireTime */ $command->setExpireTime($expireTime->getTimestamp()); } $granted->rewind(); // Forwards the pointer to the first element if ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects($granted)->setChunkSize(500); } } } icingadb-web-1.4.0/application/forms/Command/Object/CheckNowForm.php000066400000000000000000000041141516124654200253100ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use CallbackFilterIterator; use Icinga\Module\Icingadb\Command\Object\ScheduleCheckCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Web\Notification; use ipl\Orm\Model; use ipl\Web\Widget\Icon; use Iterator; use Traversable; class CheckNowForm extends CommandForm { protected $defaultAttributes = ['class' => 'inline']; public function __construct() { $this->on(self::ON_SUCCESS, function () { if (! $this->errorOccurred) { Notification::success(tp('Scheduling check..', 'Scheduling checks..', count($this->getObjects()))); } }); } protected function assembleElements() { } protected function assembleSubmitButton() { $this->addElement( 'submitButton', 'btn_submit', [ 'class' => ['link-button', 'spinner'], 'label' => [ new Icon('sync-alt'), t('Check Now') ], 'title' => t('Schedule the next active check to run immediately') ] ); } protected function getCommands(Iterator $objects): Traversable { $granted = new CallbackFilterIterator($objects, function (Model $object): bool { return $this->isGrantedOn('icingadb/command/schedule-check', $object) || ( $object->active_checks_enabled && $this->isGrantedOn('icingadb/command/schedule-check/active-only', $object) ); }); $command = new ScheduleCheckCommand(); $command->setCheckTime(time()); $command->setForced(); $granted->rewind(); // Forwards the pointer to the first element if ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects($granted)->setChunkSize(1000); } } } icingadb-web-1.4.0/application/forms/Command/Object/DeleteCommentForm.php000066400000000000000000000040531516124654200263360ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use CallbackFilterIterator; use Icinga\Module\Icingadb\Command\Object\DeleteCommentCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Web\Notification; use ipl\Orm\Model; use ipl\Web\Widget\Icon; use Iterator; use Traversable; class DeleteCommentForm extends CommandForm { protected $defaultAttributes = ['class' => 'inline']; public function __construct() { $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } $countObjects = count($this->getObjects()); Notification::success(sprintf( tp('Removed comment successfully', 'Removed %d comments successfully', $countObjects), $countObjects )); }); } protected function assembleElements() { } protected function assembleSubmitButton() { $this->addElement( 'submitButton', 'btn_submit', [ 'class' => ['cancel-button', 'spinner'], 'label' => [ new Icon('trash'), tp('Remove Comment', 'Remove Comments', count($this->getObjects())) ] ] ); } protected function getCommands(Iterator $objects): Traversable { $granted = new CallbackFilterIterator($objects, function (Model $object): bool { return $this->isGrantedOn('icingadb/command/comment/delete', $object->{$object->object_type}); }); $command = new DeleteCommentCommand(); $command->setAuthor($this->getAuth()->getUser()->getUsername()); $granted->rewind(); // Forwards the pointer to the first element if ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects($granted)->setChunkSize(500); } } } icingadb-web-1.4.0/application/forms/Command/Object/DeleteDowntimeForm.php000066400000000000000000000050561516124654200265260ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use CallbackFilterIterator; use Icinga\Module\Icingadb\Command\Object\DeleteDowntimeCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Web\Notification; use ipl\Orm\Model; use ipl\Web\Widget\Icon; use Iterator; use Traversable; class DeleteDowntimeForm extends CommandForm { protected $defaultAttributes = ['class' => 'inline']; public function __construct() { $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } $countObjects = count($this->getObjects()); Notification::success(sprintf( tp('Removed downtime successfully', 'Removed %d downtimes successfully', $countObjects), $countObjects )); }); } protected function assembleElements() { } protected function assembleSubmitButton() { $isDisabled = true; foreach ($this->getObjects() as $downtime) { if ($downtime->scheduled_by === null) { $isDisabled = false; break; } } $this->addElement( 'submitButton', 'btn_submit', [ 'class' => ['cancel-button', 'spinner'], 'disabled' => $isDisabled ?: null, 'title' => $isDisabled ? t('Downtime cannot be removed at runtime because it is based on a configured scheduled downtime.') : null, 'label' => [ new Icon('trash'), tp('Delete downtime', 'Delete downtimes', count($this->getObjects())) ] ] ); } protected function getCommands(Iterator $objects): Traversable { $granted = new CallbackFilterIterator($objects, function (Model $object): bool { return $object->scheduled_by === null && $this->isGrantedOn('icingadb/command/downtime/delete', $object->{$object->object_type}); }); $command = new DeleteDowntimeCommand(); $command->setAuthor($this->getAuth()->getUser()->getUsername()); $granted->rewind(); // Forwards the pointer to the first element if ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects($granted)->setChunkSize(250); } } } icingadb-web-1.4.0/application/forms/Command/Object/ProcessCheckResultForm.php000066400000000000000000000130231516124654200273610ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use CallbackFilterIterator; use Icinga\Module\Icingadb\Command\Object\ProcessCheckResultCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Module\Icingadb\Model\Host; use Icinga\Web\Notification; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Orm\Model; use ipl\Web\FormDecorator\IcingaFormDecorator; use ipl\Web\Widget\Icon; use Iterator; use Traversable; use function ipl\Stdlib\iterable_value_first; class ProcessCheckResultForm extends CommandForm { public function __construct() { $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } $countObjects = count($this->getObjects()); if (iterable_value_first($this->getObjects()) instanceof Host) { $message = sprintf(tp( 'Submitted passive check result successfully', 'Submitted passive check result for %d hosts successfully', $countObjects ), $countObjects); } else { $message = sprintf(tp( 'Submitted passive check result successfully', 'Submitted passive check result for %d services successfully', $countObjects ), $countObjects); } Notification::success($message); }); } protected function assembleElements() { $this->addHtml(new HtmlElement( 'div', Attributes::create(['class' => 'form-description']), new Icon('info-circle', ['class' => 'form-description-icon']), new HtmlElement( 'ul', null, new HtmlElement( 'li', null, Text::create(t('This command is used to submit passive host or service check results.')) ) ) )); $decorator = new IcingaFormDecorator(); /** @var Model $object */ $object = iterable_value_first($this->getObjects()); $this->addElement( 'select', 'status', [ 'required' => true, 'label' => t('Status'), 'description' => t('The state this check result should report'), 'options' => $object instanceof Host ? [ ProcessCheckResultCommand::HOST_UP => t('UP', 'icinga.state'), ProcessCheckResultCommand::HOST_DOWN => t('DOWN', 'icinga.state') ] : [ ProcessCheckResultCommand::SERVICE_OK => t('OK', 'icinga.state'), ProcessCheckResultCommand::SERVICE_WARNING => t('WARNING', 'icinga.state'), ProcessCheckResultCommand::SERVICE_CRITICAL => t('CRITICAL', 'icinga.state'), ProcessCheckResultCommand::SERVICE_UNKNOWN => t('UNKNOWN', 'icinga.state') ] ] ); $decorator->decorate($this->getElement('status')); $this->addElement( 'text', 'output', [ 'required' => true, 'label' => t('Output'), 'description' => t('The plugin output of this check result') ] ); $decorator->decorate($this->getElement('output')); $this->addElement( 'text', 'perfdata', [ 'allowEmpty' => true, 'label' => t('Performance Data'), 'description' => t( 'The performance data of this check result. Leave empty' . ' if this check result has no performance data' ) ] ); $decorator->decorate($this->getElement('perfdata')); } protected function assembleSubmitButton() { $this->addElement( 'submit', 'btn_submit', [ 'required' => true, 'label' => tp( 'Submit Passive Check Result', 'Submit Passive Check Results', count($this->getObjects()) ), 'data-progress-label' => tp( 'Submitting Passive Check Result', 'Submitting Passive Check Results', count($this->getObjects()) ) ] ); (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); } protected function getCommands(Iterator $objects): Traversable { $granted = new CallbackFilterIterator($objects, function (Model $object): bool { return $object->passive_checks_enabled && $this->isGrantedOn('icingadb/command/process-check-result', $object); }); $command = new ProcessCheckResultCommand(); $command->setStatus($this->getValue('status')); $command->setOutput($this->getValue('output')); $command->setPerformanceData($this->getValue('perfdata')); $granted->rewind(); // Forwards the pointer to the first element if ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects($granted)->setChunkSize(250); } } } icingadb-web-1.4.0/application/forms/Command/Object/RemoveAcknowledgementForm.php000066400000000000000000000051271516124654200301010ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use CallbackFilterIterator; use Icinga\Module\Icingadb\Command\Object\RemoveAcknowledgementCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Module\Icingadb\Model\Host; use Icinga\Web\Notification; use ipl\Orm\Model; use ipl\Web\Widget\Icon; use Iterator; use Traversable; use function ipl\Stdlib\iterable_value_first; class RemoveAcknowledgementForm extends CommandForm { public function __construct() { $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } $countObjects = count($this->getObjects()); if (iterable_value_first($this->getObjects()) instanceof Host) { $message = sprintf(tp( 'Removed acknowledgment successfully', 'Removed acknowledgment from %d hosts successfully', $countObjects ), $countObjects); } else { $message = sprintf(tp( 'Removed acknowledgment successfully', 'Removed acknowledgment from %d services successfully', $countObjects ), $countObjects); } Notification::success($message); }); } protected $defaultAttributes = ['class' => 'inline']; protected function assembleElements() { } protected function assembleSubmitButton() { $this->addElement( 'submitButton', 'btn_submit', [ 'class' => ['link-button', 'spinner'], 'label' => [ new Icon('trash'), tp('Remove acknowledgement', 'Remove acknowledgements', count($this->getObjects())) ] ] ); } protected function getCommands(Iterator $objects): Traversable { $granted = new CallbackFilterIterator($objects, function (Model $object): bool { return $this->isGrantedOn('icingadb/command/remove-acknowledgement', $object); }); $command = new RemoveAcknowledgementCommand(); $command->setAuthor($this->getAuth()->getUser()->getUsername()); $granted->rewind(); // Forwards the pointer to the first element if ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects($granted)->setChunkSize(250); } } } icingadb-web-1.4.0/application/forms/Command/Object/ScheduleCheckForm.php000066400000000000000000000112471516124654200263060ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use CallbackFilterIterator; use DateInterval; use DateTime; use Icinga\Module\Icingadb\Command\Object\ScheduleCheckCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Module\Icingadb\Model\Host; use Icinga\Web\Notification; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Orm\Model; use ipl\Web\FormDecorator\IcingaFormDecorator; use ipl\Web\Widget\Icon; use Iterator; use Traversable; use function ipl\Stdlib\iterable_value_first; class ScheduleCheckForm extends CommandForm { public function __construct() { $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } $countObjects = count($this->getObjects()); if (iterable_value_first($this->getObjects()) instanceof Host) { $message = sprintf( tp('Scheduled check successfully', 'Scheduled check for %d hosts successfully', $countObjects), $countObjects ); } else { $message = sprintf( tp('Scheduled check successfully', 'Scheduled check for %d services successfully', $countObjects), $countObjects ); } Notification::success($message); }); } protected function assembleElements() { $this->addHtml(new HtmlElement( 'div', Attributes::create(['class' => 'form-description']), new Icon('info-circle', ['class' => 'form-description-icon']), new HtmlElement( 'ul', null, new HtmlElement( 'li', null, Text::create(t( 'This command is used to schedule the next check of hosts or services. Icinga' . ' will re-queue the hosts or services to be checked at the time you specify.' )) ) ) )); $decorator = new IcingaFormDecorator(); $this->addElement( 'localDateTime', 'check_time', [ 'data-use-datetime-picker' => true, 'required' => true, 'label' => t('Check Time'), 'description' => t('Set the date and time when the check should be scheduled.'), 'value' => (new DateTime())->add(new DateInterval('PT1H')) ] ); $decorator->decorate($this->getElement('check_time')); $this->addElement( 'checkbox', 'force_check', [ 'label' => t('Force Check'), 'description' => t( 'If you select this option, Icinga will force a check regardless of both what time the' . ' scheduled check occurs and whether or not checks are enabled.' ) ] ); $decorator->decorate($this->getElement('force_check')); } protected function assembleSubmitButton() { $this->addElement( 'submit', 'btn_submit', [ 'required' => true, 'label' => tp('Schedule check', 'Schedule checks', count($this->getObjects())), 'data-progress-label' => tp('Scheduling check', 'Scheduling checks', count($this->getObjects())) ] ); (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); } protected function getCommands(Iterator $objects): Traversable { $granted = new CallbackFilterIterator($objects, function (Model $object): bool { return $this->isGrantedOn('icingadb/command/schedule-check', $object) || ( $object->active_checks_enabled && $this->isGrantedOn('icingadb/command/schedule-check/active-only', $object) ); }); $command = new ScheduleCheckCommand(); $command->setForced($this->getElement('force_check')->isChecked()); $command->setCheckTime($this->getValue('check_time')->getTimestamp()); $granted->rewind(); // Forwards the pointer to the first element if ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects($granted)->setChunkSize(1000); } } } icingadb-web-1.4.0/application/forms/Command/Object/ScheduleHostDowntimeForm.php000066400000000000000000000056041516124654200277150ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use DateInterval; use DateTime; use Icinga\Application\Config; use Icinga\Web\Notification; use ipl\Web\FormDecorator\IcingaFormDecorator; use Iterator; use Traversable; class ScheduleHostDowntimeForm extends ScheduleServiceDowntimeForm { /** @var bool */ protected $hostDowntimeAllServices; public function __construct() { $this->start = new DateTime(); $config = Config::module('icingadb'); $this->commentText = $config->get('settings', 'hostdowntime_comment_text'); $this->hostDowntimeAllServices = (bool) $config->get('settings', 'hostdowntime_all_services', false); $fixedEnd = clone $this->start; $fixed = $config->get('settings', 'hostdowntime_end_fixed', 'PT1H'); $this->fixedEnd = $fixedEnd->add(new DateInterval($fixed)); $flexibleEnd = clone $this->start; $flexible = $config->get('settings', 'hostdowntime_end_flexible', 'PT2H'); $this->flexibleEnd = $flexibleEnd->add(new DateInterval($flexible)); $flexibleDuration = $config->get('settings', 'hostdowntime_flexible_duration', 'PT2H'); $this->flexibleDuration = new DateInterval($flexibleDuration); $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } $countObjects = count($this->getObjects()); Notification::success(sprintf( tp('Scheduled downtime successfully', 'Scheduled downtime for %d hosts successfully', $countObjects), $countObjects )); }); } protected function assembleElements() { parent::assembleElements(); $decorator = new IcingaFormDecorator(); $allServices = $this->createElement( 'checkbox', 'all_services', [ 'label' => t('All Services'), 'description' => t( 'Sets downtime for all services for the matched host objects. If child options are set,' . ' all child hosts and their services will schedule a downtime too.' ), 'value' => $this->hostDowntimeAllServices ] ); $this->insertBefore($allServices, $this->getElement('child_options')); $this->registerElement($allServices); $decorator->decorate($allServices); } protected function getCommands(Iterator $objects): Traversable { if (! $this->getElement('all_services')->isChecked()) { yield from parent::getCommands($objects); } else { foreach (parent::getCommands($objects) as $command) { yield $command->setForAllServices(); } } } } icingadb-web-1.4.0/application/forms/Command/Object/ScheduleServiceDowntimeForm.php000066400000000000000000000255541516124654200304060ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use CallbackFilterIterator; use DateInterval; use DateTime; use Icinga\Application\Config; use Icinga\Module\Icingadb\Command\Object\ScheduleDowntimeCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Web\Notification; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Orm\Model; use ipl\Validator\CallbackValidator; use ipl\Web\FormDecorator\IcingaFormDecorator; use ipl\Web\Widget\Icon; use Iterator; use Traversable; class ScheduleServiceDowntimeForm extends CommandForm { /** @var DateTime downtime start */ protected $start; /** @var DateTime fixed downtime end */ protected $fixedEnd; /**@var DateTime flexible downtime end */ protected $flexibleEnd; /** @var DateInterval flexible downtime duration */ protected $flexibleDuration; /** @var mixed Comment Text */ protected $commentText; /** * Initialize this form */ public function __construct() { $this->start = new DateTime(); $config = Config::module('icingadb'); $this->commentText = $config->get('settings', 'servicedowntime_comment_text'); $fixedEnd = clone $this->start; $fixed = $config->get('settings', 'servicedowntime_end_fixed', 'PT1H'); $this->fixedEnd = $fixedEnd->add(new DateInterval($fixed)); $flexibleEnd = clone $this->start; $flexible = $config->get('settings', 'servicedowntime_end_flexible', 'PT2H'); $this->flexibleEnd = $flexibleEnd->add(new DateInterval($flexible)); $flexibleDuration = $config->get('settings', 'servicedowntime_flexible_duration', 'PT2H'); $this->flexibleDuration = new DateInterval($flexibleDuration); $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } $countObjects = count($this->getObjects()); Notification::success(sprintf( tp('Scheduled downtime successfully', 'Scheduled downtime for %d services successfully', $countObjects), $countObjects )); }); } protected function assembleElements() { $isFlexible = $this->getPopulatedValue('flexible') === 'y'; $this->addHtml(new HtmlElement( 'div', Attributes::create(['class' => 'form-description']), new Icon('info-circle', ['class' => 'form-description-icon']), new HtmlElement( 'ul', null, new HtmlElement( 'li', null, Text::create(t( 'This command is used to schedule host and service downtimes. During the downtime specified' . ' by the start and end time, Icinga will not send notifications out about the hosts and' . ' services. When the scheduled downtime expires, Icinga will send out notifications for' . ' the hosts and services as it normally would.' )) ) ) )); $decorator = new IcingaFormDecorator(); $this->addElement( 'textarea', 'comment', [ 'required' => true, 'label' => t('Comment'), 'description' => t( 'If you work with other administrators, you may find it useful to share information about' . ' the host or service that is having problems. Make sure you enter a brief description of' . ' what you are doing.' ), 'value' => $this->commentText ] ); $decorator->decorate($this->getElement('comment')); $this->addElement( 'localDateTime', 'start', [ 'data-use-datetime-picker' => true, 'required' => true, 'value' => $this->start, 'label' => t('Start Time'), 'description' => t('Set the start date and time for the downtime.') ] ); $decorator->decorate($this->getElement('start')); $this->addElement( 'localDateTime', 'end', [ 'data-use-datetime-picker' => true, 'required' => true, 'label' => t('End Time'), 'description' => t('Set the end date and time for the downtime.'), 'value' => $isFlexible ? $this->flexibleEnd : $this->fixedEnd, 'validators' => [ 'DateTime' => ['break_chain_on_failure' => true], 'Callback' => function ($value, $validator) { /** @var CallbackValidator $validator */ if ($value <= $this->getValue('start')) { $validator->addMessage(t('The end time must be greater than the start time')); return false; } if ($value <= (new DateTime())) { $validator->addMessage(t('A downtime must not be in the past')); return false; } return true; } ] ] ); $decorator->decorate($this->getElement('end')); $this->addElement( 'checkbox', 'flexible', [ 'class' => 'autosubmit', 'label' => t('Flexible'), 'description' => t( 'To make this a flexible downtime, check this option. A flexible downtime starts when the host' . ' or service enters a problem state sometime between the start and end times you specified.' . ' It then lasts as long as the duration time you enter.' ) ] ); $decorator->decorate($this->getElement('flexible')); if ($isFlexible) { $hoursInput = $this->createElement( 'number', 'hours', [ 'required' => true, 'label' => t('Duration'), 'value' => $this->flexibleDuration->h, 'min' => 0, 'validators' => [ 'Callback' => function ($value, $validator) { /** @var CallbackValidator $validator */ if ($this->getValue('minutes') == 0 && $value == 0) { $validator->addMessage(t('The duration must not be zero')); return false; } return true; } ] ] ); $this->registerElement($hoursInput); $decorator->decorate($hoursInput); $minutesInput = $this->createElement( 'number', 'minutes', [ 'required' => true, 'value' => $this->flexibleDuration->m, 'min' => 0 ] ); $this->registerElement($minutesInput); $minutesInput->addWrapper( new HtmlElement('label', null, new HtmlElement('span', null, Text::create(t('Minutes')))) ); $hoursInput->getWrapper()->on( IcingaFormDecorator::ON_ASSEMBLED, function ($hoursInputWrapper) use ($minutesInput, $hoursInput) { $hoursInputWrapper ->insertAfter($minutesInput, $hoursInput) ->getAttributes()->add('class', 'downtime-duration'); } ); $hoursInput->prependWrapper( new HtmlElement('label', null, new HtmlElement('span', null, Text::create(t('Hours')))) ); $this->add($hoursInput); } $this->addElement( 'select', 'child_options', array( 'description' => t('Schedule child downtimes.'), 'label' => t('Child Options'), 'options' => [ ScheduleDowntimeCommand::IGNORE_CHILDREN => t( 'Do nothing with children' ), ScheduleDowntimeCommand::TRIGGER_CHILDREN => t( 'Schedule a downtime for all children and trigger them by this downtime' ), ScheduleDowntimeCommand::SCHEDULE_CHILDREN => t( 'Schedule non-triggered downtime for all children' ) ] ) ); $decorator->decorate($this->getElement('child_options')); } protected function assembleSubmitButton() { $this->addElement( 'submit', 'btn_submit', [ 'required' => true, 'label' => tp('Schedule downtime', 'Schedule downtimes', count($this->getObjects())), 'data-progress-label' => tp('Scheduling downtime', 'Scheduling downtimes', count($this->getObjects())) ] ); (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); } protected function getCommands(Iterator $objects): Traversable { $granted = new CallbackFilterIterator($objects, function (Model $object): bool { return $this->isGrantedOn('icingadb/command/downtime/schedule', $object); }); $command = new ScheduleDowntimeCommand(); $command->setComment($this->getValue('comment')); $command->setAuthor($this->getAuth()->getUser()->getUsername()); $command->setStart($this->getValue('start')->getTimestamp()); $command->setEnd($this->getValue('end')->getTimestamp()); $command->setChildOption((int) $this->getValue('child_options')); if ($this->getElement('flexible')->isChecked()) { $command->setFixed(false); $command->setDuration( $this->getValue('hours') * 3600 + $this->getValue('minutes') * 60 ); } $granted->rewind(); // Forwards the pointer to the first element if ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects($granted)->setChunkSize(250); } } } icingadb-web-1.4.0/application/forms/Command/Object/SendCustomNotificationForm.php000066400000000000000000000110751516124654200302460ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use CallbackFilterIterator; use Icinga\Application\Config; use Icinga\Module\Icingadb\Command\Object\SendCustomNotificationCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Module\Icingadb\Model\Host; use Icinga\Web\Notification; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Orm\Model; use ipl\Web\FormDecorator\IcingaFormDecorator; use ipl\Web\Widget\Icon; use Iterator; use Traversable; use function ipl\Stdlib\iterable_value_first; class SendCustomNotificationForm extends CommandForm { public function __construct() { $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } $countObjects = count($this->getObjects()); if (iterable_value_first($this->getObjects()) instanceof Host) { $message = sprintf(tp( 'Sent custom notification successfully', 'Sent custom notification for %d hosts successfully', $countObjects ), $countObjects); } else { $message = sprintf(tp( 'Sent custom notification successfully', 'Sent custom notification for %d services successfully', $countObjects ), $countObjects); } Notification::success($message); }); } protected function assembleElements() { $this->addHtml(new HtmlElement( 'div', Attributes::create(['class' => 'form-description']), new Icon('info-circle', ['class' => 'form-description-icon']), new HtmlElement( 'ul', null, new HtmlElement( 'li', null, Text::create(t('This command is used to send custom notifications about hosts or services.')) ) ) )); $config = Config::module('icingadb'); $decorator = new IcingaFormDecorator(); $this->addElement( 'textarea', 'comment', [ 'required' => true, 'label' => t('Comment'), 'description' => t( 'Enter a brief description on why you\'re sending this notification. It will be sent with it.' ) ] ); $decorator->decorate($this->getElement('comment')); $this->addElement( 'checkbox', 'forced', [ 'label' => t('Forced'), 'value' => (bool) $config->get('settings', 'custom_notification_forced', false), 'description' => t( 'If you check this option, the notification is sent regardless' . ' of downtimes or whether notifications are enabled or not.' ) ] ); $decorator->decorate($this->getElement('forced')); } protected function assembleSubmitButton() { $this->addElement( 'submit', 'btn_submit', [ 'required' => true, 'label' => tp('Send custom notification', 'Send custom notifications', count($this->getObjects())), 'data-progress-label' => tp( 'Sending custom notification', 'Sending custom notifications', count($this->getObjects()) ) ] ); (new IcingaFormDecorator())->decorate($this->getElement('btn_submit')); } protected function getCommands(Iterator $objects): Traversable { $granted = new CallbackFilterIterator($objects, function (Model $object): bool { return $this->isGrantedOn('icingadb/command/send-custom-notification', $object); }); $command = new SendCustomNotificationCommand(); $command->setComment($this->getValue('comment')); $command->setForced($this->getElement('forced')->isChecked()); $command->setAuthor($this->getAuth()->getUser()->getUsername()); $granted->rewind(); // Forwards the pointer to the first element if ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects($granted)->setChunkSize(500); } } } icingadb-web-1.4.0/application/forms/Command/Object/ToggleObjectFeaturesForm.php000066400000000000000000000162121516124654200276600ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Command\Object; use CallbackFilterIterator; use Icinga\Module\Icingadb\Command\Object\ToggleObjectFeatureCommand; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Web\Notification; use ipl\Html\FormElement\CheckboxElement; use ipl\Orm\Model; use ipl\Web\FormDecorator\IcingaFormDecorator; use Iterator; use Traversable; class ToggleObjectFeaturesForm extends CommandForm { public const LEAVE_UNCHANGED = 'noop'; protected $features; protected $featureStatus; /** * ToggleFeature(s) being used to submit this form * * @var array */ protected $submittedFeatures = []; public function __construct($featureStatus) { $this->featureStatus = $featureStatus; $this->features = [ ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS => [ 'label' => t('Active Checks'), 'permission' => 'icingadb/command/feature/object/active-checks' ], ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS => [ 'label' => t('Passive Checks'), 'permission' => 'icingadb/command/feature/object/passive-checks' ], ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS => [ 'label' => t('Notifications'), 'permission' => 'icingadb/command/feature/object/notifications' ], ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER => [ 'label' => t('Event Handler'), 'permission' => 'icingadb/command/feature/object/event-handler' ], ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION => [ 'label' => t('Flap Detection'), 'permission' => 'icingadb/command/feature/object/flap-detection' ] ]; $this->getAttributes()->add('class', 'object-features'); $this->on(self::ON_SUCCESS, function () { if ($this->errorOccurred) { return; } foreach ($this->submittedFeatures as $feature => $enabled) { switch ($feature) { case ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS: if ($enabled) { $message = t('Enabled active checks successfully'); } else { $message = t('Disabled active checks successfully'); } break; case ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS: if ($enabled) { $message = t('Enabled passive checks successfully'); } else { $message = t('Disabled passive checks successfully'); } break; case ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER: if ($enabled) { $message = t('Enabled event handler successfully'); } else { $message = t('Disabled event handler checks successfully'); } break; case ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION: if ($enabled) { $message = t('Enabled flap detection successfully'); } else { $message = t('Disabled flap detection successfully'); } break; case ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS: if ($enabled) { $message = t('Enabled notifications successfully'); } else { $message = t('Disabled notifications successfully'); } break; default: $message = t('Invalid feature option'); break; } Notification::success($message); } }); } protected function assembleElements() { $decorator = new IcingaFormDecorator(); foreach ($this->features as $feature => $spec) { $options = [ 'class' => 'autosubmit', 'disabled' => $this->featureStatus instanceof Model ? ! $this->isGrantedOn($spec['permission'], $this->featureStatus) : false, 'label' => $spec['label'] ]; if ($this->featureStatus[$feature] === 2) { $this->addElement( 'select', $feature, $options + [ 'description' => t('Multiple Values'), 'options' => [ self::LEAVE_UNCHANGED => t('Leave Unchanged'), t('Disable All'), t('Enable All') ], 'value' => self::LEAVE_UNCHANGED ] ); $decorator->decorate($this->getElement($feature)); $this->getElement($feature) ->getWrapper() ->getAttributes() ->add('class', 'indeterminate'); } else { $options['value'] = (bool) $this->featureStatus[$feature]; $this->addElement('checkbox', $feature, $options); $decorator->decorate($this->getElement($feature)); } } } protected function assembleSubmitButton() { } protected function getCommands(Iterator $objects): Traversable { foreach ($this->features as $feature => $spec) { if ($this->getElement($feature) instanceof CheckboxElement) { $state = $this->getElement($feature)->isChecked(); } else { $state = $this->getElement($feature)->getValue(); } if ($state === self::LEAVE_UNCHANGED || (int) $state === (int) $this->featureStatus[$feature]) { continue; } $granted = new CallbackFilterIterator($objects, function (Model $object) use ($spec): bool { return $this->isGrantedOn($spec['permission'], $object); }); $command = new ToggleObjectFeatureCommand(); $command->setFeature($feature); $command->setEnabled((int) $state); $granted->rewind(); // Forwards the pointer to the first element if ($granted->valid()) { $this->submittedFeatures[$command->getFeature()] ??= $command->getEnabled(); // Chunk objects to avoid timeouts with large sets yield $command->setObjects($granted)->setChunkSize(1000); } } } } icingadb-web-1.4.0/application/forms/DatabaseConfigForm.php000066400000000000000000000017011516124654200236540ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms; use Icinga\Data\ResourceFactory; use Icinga\Forms\ConfigForm; class DatabaseConfigForm extends ConfigForm { public function init() { $this->setSubmitLabel(t('Save Changes')); } public function createElements(array $formData) { $dbResources = ResourceFactory::getResourceConfigs('db')->keys(); $this->addElement('select', 'icingadb_resource', [ 'description' => t('Database resource'), 'label' => t('Database'), 'multiOptions' => array_merge( ['' => sprintf(' - %s - ', t('Please choose'))], array_combine($dbResources, $dbResources) ), 'disable' => [''], 'required' => true, 'value' => '' ]); } } icingadb-web-1.4.0/application/forms/Navigation/000077500000000000000000000000001516124654200215655ustar00rootroot00000000000000icingadb-web-1.4.0/application/forms/Navigation/ActionForm.php000066400000000000000000000030661516124654200243440ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Navigation; use Icinga\Exception\ConfigurationError; use Icinga\Forms\Navigation\NavigationItemForm; use Icinga\Module\Icingadb\Common\Auth; class ActionForm extends NavigationItemForm { use Auth; /** * The name of the restriction to which the filter should be applied * * @var string */ protected $restriction; public function createElements(array $formData) { parent::createElements($formData); $this->addElement( 'text', 'filter', array( 'allowEmpty' => true, 'label' => $this->translate('Filter'), 'description' => $this->translate( 'Display this action only for objects matching this filter. Leave it blank' . ' if you want this action being displayed regardless of the object' ) ) ); } public function isValid($formData): bool { if (! parent::isValid($formData)) { return false; } if (($filterString = $this->getValue('filter')) !== null) { try { $this->parseRestriction($filterString, $this->restriction); } catch (ConfigurationError $err) { $this->getElement('filter')->addError($err->getMessage()); return false; } } return true; } } icingadb-web-1.4.0/application/forms/Navigation/IcingadbHostActionForm.php000066400000000000000000000004241516124654200266160ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Navigation; class IcingadbHostActionForm extends ActionForm { protected $restriction = 'icingadb/filter/hosts'; } icingadb-web-1.4.0/application/forms/Navigation/IcingadbServiceActionForm.php000066400000000000000000000004321516124654200273000ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms\Navigation; class IcingadbServiceActionForm extends ActionForm { protected $restriction = 'icingadb/filter/services'; } icingadb-web-1.4.0/application/forms/RedisConfigForm.php000066400000000000000000000642561516124654200232340ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Forms; use Closure; use Exception; use Icinga\Application\Config; use Icinga\Application\Icinga; use Icinga\Application\Logger; use Icinga\Exception\AlreadyExistsException; use Icinga\Exception\IcingaException; use Icinga\Exception\NotWritableError; use Icinga\File\Storage\LocalFileStorage; use Icinga\File\Storage\TemporaryLocalFileStorage; use Icinga\Forms\ConfigForm; use Icinga\Module\Icingadb\Common\IcingaRedis; use Icinga\Web\Form; use ipl\Validator\PrivateKeyValidator; use ipl\Validator\X509CertValidator; use Throwable; use Zend_Config_Exception; use Zend_Form_Decorator_Abstract; use Zend_Validate_Callback; class RedisConfigForm extends ConfigForm { public function init() { $this->setSubmitLabel(t('Save Changes')); $this->setValidatePartial(true); } public function createElements(array $formData) { $this->addElement('checkbox', 'redis_tls', [ 'label' => t('Use TLS'), 'description' => t('Encrypt connections to Redis via TLS'), 'autosubmit' => true ]); $this->addElement('hidden', 'redis_ca'); $this->addElement('hidden', 'redis_cert'); $this->addElement('hidden', 'redis_key'); $this->addElement('hidden', 'clear_redis_ca', ['ignore' => true]); $this->addElement('hidden', 'clear_redis_cert', ['ignore' => true]); $this->addElement('hidden', 'clear_redis_key', ['ignore' => true]); $useTls = isset($formData['redis_tls']) && $formData['redis_tls']; if ($useTls) { $this->addElement('textarea', 'redis_ca_pem', [ 'label' => t('Redis CA Certificate'), 'description' => sprintf( t('Verify the peer using this PEM-encoded CA certificate ("%s...")'), '-----BEGIN CERTIFICATE-----' ), 'required' => true, 'ignore' => true, 'validators' => [$this->wrapIplValidator(X509CertValidator::class, 'redis_ca_pem')] ]); $this->addElement('textarea', 'redis_cert_pem', [ 'label' => t('Client Certificate'), 'description' => sprintf( t('Authenticate using this PEM-encoded client certificate ("%s...")'), '-----BEGIN CERTIFICATE-----' ), 'ignore' => true, 'allowEmpty' => false, 'validators' => [ $this->wrapIplValidator(X509CertValidator::class, 'redis_cert_pem', function ($value) { if (! $value && $this->getElement('redis_key_pem')->getValue()) { $this->getElement('redis_cert_pem')->addError(t( 'Either both a client certificate and its private key or none of them must be specified' )); } return true; }) ] ]); $this->addElement('textarea', 'redis_key_pem', [ 'label' => t('Client Key'), 'description' => sprintf( t('Authenticate using this PEM-encoded private key ("%s...")'), '-----BEGIN PRIVATE KEY-----' ), 'ignore' => true, 'allowEmpty' => false, 'validators' => [ $this->wrapIplValidator(PrivateKeyValidator::class, 'redis_key_pem', function ($value) { if (! $value && $this->getElement('redis_cert_pem')->getValue()) { $this->getElement('redis_key_pem')->addError(t( 'Either both a client certificate and its private key or none of them must be specified' )); } return true; }) ] ]); } $this->addDisplayGroup( ['redis_tls', 'redis_insecure', 'redis_ca_pem', 'redis_cert_pem', 'redis_key_pem'], 'redis', [ 'decorators' => [ 'FormElements', ['HtmlTag', ['tag' => 'div']], [ 'Description', ['tag' => 'span', 'class' => 'description', 'placement' => 'prepend'] ], 'Fieldset' ], 'description' => t( 'Secure connections. If you are running a high availability zone' . ' with two masters, the following applies to both of them.' ), 'legend' => t('General') ] ); if (isset($formData['skip_validation']) && $formData['skip_validation']) { // In case another error occured and the checkbox was displayed before static::addSkipValidationCheckbox($this); } if ($useTls && isset($formData['redis_insecure']) && $formData['redis_insecure']) { // In case another error occured and the checkbox was displayed before static::addInsecureCheckboxIfTls($this); } $redisPortDescription = t(sprintf( "Defaults to %d as the Redis® open source server provided by" . ' the "icingadb-redis" package listens on that port.', IcingaRedis::DEFAULT_PORT )); $redisDatabaseDescription = t(sprintf( "Numerical database identifier, defaults to %d. This only needs to be changed" . ' if Icinga 2 is configured to write to another database index.', IcingaRedis::DEFAULT_DATABASE )); $redisUsernameDescription = t( 'Authentication username, requires a password being set as well. This field is necessary when' . ' connecting to a redis instance that requires authentication beyond the default user.' ); $redisPasswordDescription = t( 'Authentication password. May be used alone when logging in as the' . ' default user or together with a username.' ); $this->addElement('text', 'redis1_host', [ 'description' => t('Redis Host'), 'label' => t('Redis Host'), 'required' => true ]); $this->addElement('number', 'redis1_port', [ 'description' => $redisPortDescription, 'label' => t('Redis Port'), 'placeholder' => IcingaRedis::DEFAULT_PORT ]); $this->addElement('number', 'redis1_database', [ 'description' => $redisDatabaseDescription, 'label' => t('Redis Database'), 'placeholder' => IcingaRedis::DEFAULT_DATABASE ]); $this->addElement('text', 'redis1_username', [ 'description' => $redisUsernameDescription, 'label' => t('Redis Username') ]); $this->addElement('password', 'redis1_password', [ 'description' => $redisPasswordDescription, 'label' => t('Redis Password'), 'renderPassword' => true, 'autocomplete' => 'new-password' ]); $this->addDisplayGroup( ['redis1_host', 'redis1_port', 'redis1_database', 'redis1_username', 'redis1_password'], 'redis1', [ 'decorators' => [ 'FormElements', ['HtmlTag', ['tag' => 'div']], [ 'Description', ['tag' => 'span', 'class' => 'description', 'placement' => 'prepend'] ], 'Fieldset' ], 'description' => t( 'Redis connection details of your Icinga host. If you are running a high' . ' availability zone with two masters, this is your configuration master.' ), 'legend' => t('Primary Icinga Master') ] ); $this->addElement('text', 'redis2_host', [ 'description' => t('Redis Host'), 'label' => t('Redis Host'), ]); $this->addElement('number', 'redis2_port', [ 'description' => $redisPortDescription, 'label' => t('Redis Port'), 'placeholder' => IcingaRedis::DEFAULT_PORT ]); $this->addElement('number', 'redis2_database', [ 'description' => $redisDatabaseDescription, 'label' => t('Redis Database'), 'placeholder' => IcingaRedis::DEFAULT_DATABASE ]); $this->addElement('text', 'redis2_username', [ 'description' => $redisUsernameDescription, 'label' => t('Redis Username') ]); $this->addElement('password', 'redis2_password', [ 'description' => $redisPasswordDescription, 'label' => t('Redis Password'), 'renderPassword' => true, 'autocomplete' => 'new-password' ]); $this->addDisplayGroup( ['redis2_host', 'redis2_port', 'redis2_database', 'redis2_username', 'redis2_password'], 'redis2', [ 'decorators' => [ 'FormElements', ['HtmlTag', ['tag' => 'div']], [ 'Description', ['tag' => 'span', 'class' => 'description', 'placement' => 'prepend'] ], 'Fieldset' ], 'description' => t( 'If you are running a high availability zone with two masters,' . ' please provide the Redis connection details of the secondary master.' ), 'legend' => t('Secondary Icinga Master') ] ); } public static function addSkipValidationCheckbox(Form $form) { $form->addElement( 'checkbox', 'skip_validation', [ 'order' => 0, 'ignore' => true, 'label' => t('Skip Validation'), 'description' => t( 'Check this box to enforce changes without validating that Redis is available.' ) ] ); } public static function addInsecureCheckboxIfTls(Form $form) { if ($form->getElement('redis_insecure') !== null) { return; } $form->addElement( 'checkbox', 'redis_insecure', [ 'order' => 1, 'label' => t('Insecure'), 'description' => t('Don\'t verify the peer') ] ); $displayGroup = $form->getDisplayGroup('redis'); $elements = $displayGroup->getElements(); $elements['redis_insecure'] = $form->getElement('redis_insecure'); $displayGroup->setElements($elements); } public function isValid($formData) { if (! parent::isValid($formData)) { return false; } if (($el = $this->getElement('skip_validation')) === null || ! $el->isChecked()) { if (! static::checkRedis($this)) { if ($el === null) { static::addSkipValidationCheckbox($this); if ($this->getElement('redis_tls')->isChecked()) { static::addInsecureCheckboxIfTls($this); } } return false; } } return true; } public function isValidPartial(array $formData) { if (! parent::isValidPartial($formData)) { return false; } $useTls = $this->getElement('redis_tls')->isChecked(); foreach (['ca', 'cert', 'key'] as $name) { $textareaName = 'redis_' . $name . '_pem'; $clearName = 'clear_redis_' . $name; if ($useTls) { $this->getElement($clearName)->setValue(null); $pemPath = $this->getValue('redis_' . $name); if ($pemPath && ! isset($formData[$textareaName]) && ! $formData[$clearName]) { $this->getElement($textareaName)->setValue(@file_get_contents($pemPath)); } } if (isset($formData[$textareaName]) && ! $formData[$textareaName]) { $this->getElement($clearName)->setValue(true); } } if ($this->getElement('backend_validation')->isChecked()) { if (! static::checkRedis($this)) { if ($this->getElement('redis_tls')->isChecked()) { static::addInsecureCheckboxIfTls($this); } return false; } } return true; } public function onRequest() { $errors = []; $redisConfig = $this->config->getSection('redis'); if ($redisConfig->get('tls', false)) { foreach (['ca', 'cert', 'key'] as $name) { $path = $redisConfig->get($name, ''); if (file_exists($path)) { try { $redisConfig[$name . '_pem'] = file_get_contents($path); } catch (Exception $e) { $errors['redis_' . $name . '_pem'] = sprintf( t('Failed to read file "%s": %s'), $path, $e->getMessage() ); } } } } $connectionConfig = Config::fromIni( join(DIRECTORY_SEPARATOR, [dirname($this->config->getConfigFile()), 'redis.ini']) ); $this->config->setSection('redis1', [ 'host' => $connectionConfig->get('redis1', 'host'), 'port' => $connectionConfig->get('redis1', 'port'), 'database' => $connectionConfig->get('redis1', 'database'), 'username' => $connectionConfig->get('redis1', 'username'), 'password' => $connectionConfig->get('redis1', 'password') ]); $this->config->setSection('redis2', [ 'host' => $connectionConfig->get('redis2', 'host'), 'port' => $connectionConfig->get('redis2', 'port'), 'database' => $connectionConfig->get('redis2', 'database'), 'username' => $connectionConfig->get('redis2', 'username'), 'password' => $connectionConfig->get('redis2', 'password') ]); parent::onRequest(); foreach ($errors as $elementName => $message) { $this->getElement($elementName)->addError($message); } } public function onSuccess() { $storage = new LocalFileStorage(Icinga::app()->getStorageDir( join(DIRECTORY_SEPARATOR, ['modules', 'icingadb', 'redis']) )); $useTls = $this->getElement('redis_tls')->isChecked(); $pem = null; foreach (['ca', 'cert', 'key'] as $name) { $textarea = $this->getElement('redis_' . $name . '_pem'); if ($useTls && $textarea !== null && ($pem = $textarea->getValue())) { $pemFile = md5($pem) . '-' . $name . '.pem'; if (! $storage->has($pemFile)) { try { $storage->create($pemFile, $pem); } catch (NotWritableError $e) { $textarea->addError($e->getMessage()); return false; } catch (AlreadyExistsException $e) { $textarea->addError($e->getMessage()); return false; } } $this->getElement('redis_' . $name)->setValue($storage->resolvePath($pemFile)); } if ((! $useTls && $this->getElement('clear_redis_' . $name)->getValue()) || ($useTls && ! $pem)) { $pemPath = $this->getValue('redis_' . $name); if ($pemPath && $storage->has(($pemFile = basename($pemPath)))) { try { $storage->delete($pemFile); $this->getElement('redis_' . $name)->setValue(null); } catch (NotWritableError $e) { $this->addError($e->getMessage()); return false; } } } } $connectionConfig = Config::fromIni( join(DIRECTORY_SEPARATOR, [dirname($this->config->getConfigFile()), 'redis.ini']) ); $redis1Host = $this->getValue('redis1_host'); $redis1Port = $this->getValue('redis1_port'); $redis1Database = $this->getValue('redis1_database'); $redis1Username = $this->getValue('redis1_username'); $redis1Password = $this->getValue('redis1_password'); $redis1Section = $connectionConfig->getSection('redis1'); $redis1Section['host'] = $redis1Host; $this->getElement('redis1_host')->setValue(null); $connectionConfig->setSection('redis1', $redis1Section); if (! empty($redis1Port)) { $redis1Section['port'] = $redis1Port; $this->getElement('redis1_port')->setValue(null); } else { $redis1Section['port'] = null; } if (! empty($redis1Database)) { $redis1Section['database'] = $redis1Database; $this->getElement('redis1_database')->setValue(null); } else { $redis1Section['database'] = null; } if (! empty($redis1Username)) { $redis1Section['username'] = $redis1Username; $this->getElement('redis1_username')->setValue(null); } else { $redis1Section['username'] = null; } if (! empty($redis1Password)) { $redis1Section['password'] = $redis1Password; $this->getElement('redis1_password')->setValue(null); } else { $redis1Section['password'] = null; } if (! array_filter($redis1Section->toArray())) { $connectionConfig->removeSection('redis1'); } $redis2Host = $this->getValue('redis2_host'); $redis2Port = $this->getValue('redis2_port'); $redis2Database = $this->getValue('redis2_database'); $redis2Username = $this->getValue('redis2_username'); $redis2Password = $this->getValue('redis2_password'); $redis2Section = $connectionConfig->getSection('redis2'); if (! empty($redis2Host)) { $redis2Section['host'] = $redis2Host; $this->getElement('redis2_host')->setValue(null); $connectionConfig->setSection('redis2', $redis2Section); } else { $redis2Section['host'] = null; } if (! empty($redis2Port)) { $redis2Section['port'] = $redis2Port; $this->getElement('redis2_port')->setValue(null); $connectionConfig->setSection('redis2', $redis2Section); } else { $redis2Section['port'] = null; } if (! empty($redis2Database)) { $redis2Section['database'] = $redis2Database; $this->getElement('redis2_database')->setValue(null); } else { $redis2Section['database'] = null; } if (! empty($redis2Username)) { $redis2Section['username'] = $redis2Username; $this->getElement('redis2_username')->setValue(null); } else { $redis2Section['username'] = null; } if (! empty($redis2Password)) { $redis2Section['password'] = $redis2Password; $this->getElement('redis2_password')->setValue(null); } else { $redis2Section['password'] = null; } if (! array_filter($redis2Section->toArray())) { $connectionConfig->removeSection('redis2'); } try { $connectionConfig->saveIni(); } catch (NotWritableError | Zend_Config_Exception $e) { $this->addDecorator('ViewScript', array( 'viewModule' => 'default', 'viewScript' => 'showConfiguration.phtml', 'errorMessage' => $e->getMessage(), 'configString' => $connectionConfig, 'filePath' => $connectionConfig->getConfigFile(), 'placement' => Zend_Form_Decorator_Abstract::PREPEND )); return false; } catch (Throwable $e) { $this->addError($e->getMessage()); Logger::error($e->getMessage()); Logger::debug(IcingaException::getConfidentialTraceAsString($e)); return false; } return parent::onSuccess(); } public function addSubmitButton() { parent::addSubmitButton() ->getElement('btn_submit') ->setDecorators(['ViewHelper']); $this->addElement( 'submit', 'backend_validation', [ 'ignore' => true, 'label' => t('Validate Configuration'), 'data-progress-label' => t('Validation In Progress'), 'decorators' => ['ViewHelper'] ] ); $this->addDisplayGroup( ['btn_submit', 'backend_validation'], 'submit_validation', [ 'decorators' => [ 'FormElements', ['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']] ] ] ); return $this; } public static function checkRedis(Form $form): bool { $sections = []; $storage = new TemporaryLocalFileStorage(); foreach (ConfigForm::transformEmptyValuesToNull($form->getValues()) as $sectionAndPropertyName => $value) { if ($value !== null) { list($section, $property) = explode('_', $sectionAndPropertyName, 2); if (in_array($property, ['ca', 'cert', 'key'])) { $storage->create("$property.pem", $value); $value = $storage->resolvePath("$property.pem"); } $sections[$section][$property] = $value; } } $ignoredTextAreas = [ 'ca' => 'redis_ca_pem', 'cert' => 'redis_cert_pem', 'key' => 'redis_key_pem' ]; foreach ($ignoredTextAreas as $name => $textareaName) { if (($textarea = $form->getElement($textareaName)) !== null) { if (($pem = $textarea->getValue())) { if ($storage->has("$name.pem")) { $storage->update("$name.pem", $pem); } else { $storage->create("$name.pem", $pem); $sections['redis'][$name] = $storage->resolvePath("$name.pem"); } } elseif ($storage->has("$name.pem")) { $storage->delete("$name.pem"); unset($sections['redis'][$name]); } } } $moduleConfig = new Config(); $moduleConfig->setSection('redis', $sections['redis']); $redisConfig = new Config(); $redisConfig->setSection('redis1', $sections['redis1'] ?? []); $redisConfig->setSection('redis2', $sections['redis2'] ?? []); try { $redis1 = IcingaRedis::getPrimaryRedis($moduleConfig, $redisConfig); } catch (Exception $e) { $form->warning(sprintf( t('Failed to connect to primary Redis: %s'), $e->getMessage() )); return false; } if (IcingaRedis::getLastIcingaHeartbeat($redis1) === null) { $form->warning(t('Primary connection established but failed to verify Icinga is connected as well.')); return false; } try { $redis2 = IcingaRedis::getSecondaryRedis($moduleConfig, $redisConfig); } catch (Exception $e) { $form->warning(sprintf(t('Failed to connect to secondary Redis: %s'), $e->getMessage())); return false; } if ($redis2 !== null && IcingaRedis::getLastIcingaHeartbeat($redis2) === null) { $form->warning(t('Secondary connection established but failed to verify Icinga is connected as well.')); return false; } $form->info(t('The configuration has been successfully validated.')); return true; } /** * Wraps the given IPL validator class into a callback validator * for usage as the only validator of the element given by name. * * @param string $cls IPL validator class FQN * @param string $element Form element name * @param ?Closure $additionalValidator * * @return array Callback validator */ private function wrapIplValidator(string $cls, string $element, ?Closure $additionalValidator = null): array { return [ 'Callback', false, [ 'callback' => function ($v) use ($cls, $element, $additionalValidator) { if ($additionalValidator !== null) { if (! $additionalValidator($v)) { return false; } } if (! $v) { return true; } $validator = new $cls(); $valid = $validator->isValid($v); if (! $valid) { /** @var Zend_Validate_Callback $callbackValidator */ $callbackValidator = $this->getElement($element)->getValidator('Callback'); $callbackValidator->setMessage( $validator->getMessages()[0], Zend_Validate_Callback::INVALID_VALUE ); } return $valid; } ] ]; } } icingadb-web-1.4.0/application/views/000077500000000000000000000000001516124654200174755ustar00rootroot00000000000000icingadb-web-1.4.0/application/views/scripts/000077500000000000000000000000001516124654200211645ustar00rootroot00000000000000icingadb-web-1.4.0/application/views/scripts/joystickPagination-icingadb.phtml000066400000000000000000000122601516124654200276420ustar00rootroot00000000000000translate('%s: Show %s %u to %u out of %u', 'pagination.joystick'); $flipUrl = clone $baseUrl; if ($flipUrl->getParam('flipped')) { $flipUrl->remove('flipped'); } else { $flipUrl->setParam('flipped'); } if ($flipUrl->hasParam('page')) { $flipUrl->setParam('page', implode(',', array_reverse(explode(',', $flipUrl->getParam('page'))))); } if ($flipUrl->hasParam('limit')) { $flipUrl->setParam('limit', implode(',', array_reverse(explode(',', $flipUrl->getParam('limit'))))); } $yAxisTotalItem = $yAxisPaginator->count(); $yAxisItemCountPerPage = $yAxisPaginator->getLimit() ?? $yAxisTotalItem; $totalYAxisPages = ceil($yAxisTotalItem / $yAxisItemCountPerPage); $currentYAxisPage = round($yAxisPaginator->getOffset() / $yAxisItemCountPerPage) + 1; $prevYAxisPage = $currentYAxisPage > 1 ? $currentYAxisPage - 1 : null; $nextYAxisPage = $currentYAxisPage < $totalYAxisPages ? $currentYAxisPage + 1 : null; $xAxisTotalItem = $xAxisPaginator->count(); $xAxisItemCountPerPage = $xAxisPaginator->getLimit() ?? $xAxisTotalItem; $totalXAxisPages = ceil($xAxisTotalItem / $xAxisItemCountPerPage); $currentXAxisPage = round($xAxisPaginator->getOffset() / $xAxisItemCountPerPage) + 1; $prevXAxisPage = $currentXAxisPage > 1 ? $currentXAxisPage - 1 : null; $nextXAxisPage = $currentXAxisPage < $totalXAxisPages ? $currentXAxisPage + 1 : null; ?> flippable): ?>
  qlink( '', $baseUrl, array( 'page' => $currentXAxisPage . ',' . $prevYAxisPage ), array( 'icon' => 'up-open', 'data-base-target' => '_self', 'title' => sprintf( $showText, $this->translate('Y-Axis', 'pagination.joystick'), $this->translate('hosts', 'pagination.joystick'), ($prevYAxisPage - 1) * $xAxisItemCountPerPage + 1, $prevYAxisPage * $xAxisItemCountPerPage, $yAxisTotalItem ) ) ); ?> icon('up-open'); ?>  
qlink( '', $baseUrl, array( 'page' => $prevXAxisPage . ',' . $currentYAxisPage ), array( 'icon' => 'left-open', 'data-base-target' => '_self', 'title' => sprintf( $showText, $this->translate('X-Axis', 'pagination.joystick'), $this->translate('services', 'pagination.joystick'), ($prevXAxisPage - 1) * $xAxisItemCountPerPage + 1, $prevXAxisPage * $xAxisItemCountPerPage, $xAxisTotalItem ) ) ); ?> icon('left-open'); ?> qlink( '', $flipUrl, null, array( 'icon' => 'arrows-cw', 'data-base-target' => '_self', 'title' => $this->translate('Flip grid axes') ) ) ?>   qlink( '', $baseUrl, array( 'page' => $nextXAxisPage . ',' . $currentYAxisPage ), array( 'icon' => 'right-open', 'data-base-target' => '_self', 'title' => sprintf( $showText, $this->translate('X-Axis', 'pagination.joystick'), $this->translate('services', 'pagination.joystick'), $currentXAxisPage * $xAxisItemCountPerPage + 1, $nextXAxisPage === $totalXAxisPages ? $xAxisItemCountPerPage : $nextXAxisPage * $xAxisItemCountPerPage, $xAxisTotalItem ) ), false ); ?> icon('right-open'); ?>
  qlink( '', $baseUrl, array( 'page' => $currentXAxisPage . ',' . $nextYAxisPage ), array( 'icon' => 'down-open', 'data-base-target' => '_self', 'title' => sprintf( $showText, $this->translate('Y-Axis', 'pagination.joystick'), $this->translate('hosts', 'pagination.joystick'), $currentYAxisPage * $yAxisItemCountPerPage + 1, $nextYAxisPage === $totalYAxisPages ? $yAxisItemCountPerPage : $nextYAxisPage * $yAxisItemCountPerPage, $yAxisTotalItem ) ) ); ?> icon('down-open'); ?>  
icingadb-web-1.4.0/application/views/scripts/services/000077500000000000000000000000001516124654200230075ustar00rootroot00000000000000icingadb-web-1.4.0/application/views/scripts/services/grid-flipped.phtml000066400000000000000000000141001516124654200264170ustar00rootroot00000000000000compact): ?> controls ?>
translate('No services found matching the filter.') ?>
$_) { $serviceFilter->orFilter(Filter::where('service.name', $serviceDescription)); } ?> $hostAlias): ?> $serviceDisplayName): ?> horizontalPaginator->count(); $horizontalItemsPerPage = $this->horizontalPaginator->getLimit() ?? $horizontalTotalItems; $horizontalTotalPages = ceil($horizontalTotalItems / $horizontalItemsPerPage); $verticalTotalItems = $this->verticalPaginator->count(); $verticalItemsPerPage = $this->verticalPaginator->getLimit() ?? $verticalTotalItems; $verticalTotalPages = ceil($verticalTotalItems / $verticalItemsPerPage); if (! $this->compact && $horizontalTotalPages > 1): ?> compact && $verticalTotalPages > 1): ?>
partial( 'joystickPagination-icingadb.phtml', 'default', array( 'flippable' => true, 'baseUrl' => $baseUrl, 'xAxisPaginator' => $horizontalPaginator, 'yAxisPaginator' => $verticalPaginator ) ) ?>
qlink( $this->ellipsis($hostAlias, 24), Url::fromPath('icingadb/services')->addFilter( Filter::matchAll($serviceFilter, Filter::where('host.name', $hostName)) ), null, array('title' => sprintf($this->translate('List all reported services on host %s'), $hostAlias)), false ) ?>
$_) { $hostFilter->orFilter(Filter::where('host.name', $hostName)); } echo $this->qlink( $serviceDisplayName, Url::fromPath('icingadb/services')->addFilter( Filter::matchAll($hostFilter, Filter::where('service.name', $serviceDescription)) ), null, array('title' => sprintf( $this->translate('List all services with the name "%s" on all reported hosts'), $serviceDisplayName )) ); ?> protectId($service->host_name . '_' . $service->name . '_desc') ?> escape($service->state->output) ?> qlink( '', 'icingadb/service', array( 'host.name' => $hostName, 'name' => $serviceDescription ), array( 'aria-describedby' => $ariaDescribedById, 'aria-label' => sprintf( $this->translate('Show detailed information for service %s on host %s'), $service->display_name, $service->host_display_name ), 'class' => 'service-grid-link state-' . $service->state->getStateText() . ($service->state->is_handled ? ' handled' : ''), 'title' => $service->state->output ) ) ?> qlink( $this->translate('Load more'), $baseUrl, array( 'limit' => ($horizontalItemsPerPage + 20) . ',' . $verticalItemsPerPage ), array( 'class' => 'action-link', 'data-base-target' => '_self' ) ) ?>
qlink( $this->translate('Load more'), $baseUrl, array( 'limit' => $horizontalItemsPerPage . ',' . ($verticalItemsPerPage + 20) ), array( 'class' => 'action-link', 'data-base-target' => '_self' ) ) ?>
icingadb-web-1.4.0/application/views/scripts/services/grid.phtml000066400000000000000000000141501516124654200250030ustar00rootroot00000000000000compact): ?> controls ?>
translate('No services found matching the filter.') ?>
$_) { $hostFilter->orFilter(Filter::where('host.name', $hostName)); } ?> $serviceDisplayName):?> $hostDisplayName): ?> horizontalPaginator->count(); $horizontalItemsPerPage = $this->horizontalPaginator->getLimit() ?? $horizontalTotalItems; $horizontalTotalPages = ceil($horizontalTotalItems / $horizontalItemsPerPage); $verticalTotalItems = $this->verticalPaginator->count(); $verticalItemsPerPage = $this->verticalPaginator->getLimit() ?? $verticalTotalItems; $verticalTotalPages = ceil($verticalTotalItems / $verticalItemsPerPage); if (! $this->compact && $horizontalTotalPages > 1): ?> compact && $verticalTotalPages > 1): ?>
partial( 'joystickPagination-icingadb.phtml', 'default', array( 'flippable' => true, 'baseUrl' => $baseUrl, 'xAxisPaginator' => $horizontalPaginator, 'yAxisPaginator' => $verticalPaginator ) ); ?>
qlink( $this->ellipsis($serviceDisplayName, 24), Url::fromPath('icingadb/services')->addFilter( Filter::matchAll($hostFilter, Filter::where('service.name', $serviceDescription)) ), null, array('title' => sprintf( $this->translate('List all services with the name "%s" on all reported hosts'), $serviceDisplayName )), false ) ?>
$_) { $serviceFilter->orFilter(Filter::where('service.name', $serviceName)); } echo $this->qlink( $hostDisplayName, Url::fromPath('icingadb/services')->addFilter( Filter::matchAll($serviceFilter, Filter::where('host.name', $hostName)) ), null, array('title' => sprintf($this->translate('List all reported services on host %s'), $hostDisplayName)) ); ?> protectId($service->host_name . '_' . $service->name . '_desc') ?> escape($service->state->output) ?> qlink( '', 'icingadb/service', array( 'host.name' => $hostName, 'name' => $serviceDescription ), array( 'aria-describedby' => $ariaDescribedById, 'aria-label' => sprintf( $this->translate('Show detailed information for service %s on host %s'), $service->display_name, $service->host_display_name ), 'class' => 'service-grid-link state-' . $service->state->getStateText() . ($service->state->is_handled ? ' handled' : ''), 'title' => $service->state->output ) ) ?> qlink( $this->translate('Load more'), $baseUrl, array( 'limit' => ( $horizontalItemsPerPage + 20) . ',' . $verticalItemsPerPage ), array( 'class' => 'action-link', 'data-base-target' => '_self' ) ) ?>
qlink( $this->translate('Load more'), $baseUrl, array( 'limit' => $horizontalItemsPerPage . ',' . ($verticalItemsPerPage + 20) ), array( 'class' => 'action-link', 'data-base-target' => '_self' ) ) ?>
icingadb-web-1.4.0/configuration.php000066400000000000000000000525741516124654200174320ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb { use Icinga\Application\Icinga; use Icinga\Authentication\Auth; use Icinga\Module\Icingadb\Web\Navigation\Renderer\HostProblemsBadge; use Icinga\Module\Icingadb\Web\Navigation\Renderer\ServiceProblemsBadge; use Icinga\Util\StringHelper; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; /** @var \Icinga\Application\Modules\Module $this */ $auth = Auth::getInstance(); $authenticated = Icinga::app()->isWeb() && $auth->isAuthenticated(); $this->provideSetupWizard('Icinga\Module\Icingadb\Setup\IcingaDbWizard'); $this->providePermission( 'icingadb/command/*', $this->translate('Allow all commands') ); $this->providePermission( 'icingadb/command/schedule-check', $this->translate('Allow to schedule host and service checks') ); $this->providePermission( 'icingadb/command/schedule-check/active-only', $this->translate('Allow to schedule host and service checks (Only on objects with active checks enabled)') ); $this->providePermission( 'icingadb/command/acknowledge-problem', $this->translate('Allow to acknowledge host and service problems') ); $this->providePermission( 'icingadb/command/remove-acknowledgement', $this->translate('Allow to remove problem acknowledgements') ); $this->providePermission( 'icingadb/command/comment/*', $this->translate('Allow to add and delete host and service comments') ); $this->providePermission( 'icingadb/command/comment/add', $this->translate('Allow to add host and service comments') ); $this->providePermission( 'icingadb/command/comment/delete', $this->translate('Allow to delete host and service comments') ); $this->providePermission( 'icingadb/command/downtime/*', $this->translate('Allow to schedule and delete host and service downtimes') ); $this->providePermission( 'icingadb/command/downtime/schedule', $this->translate('Allow to schedule host and service downtimes') ); $this->providePermission( 'icingadb/command/downtime/delete', $this->translate('Allow to delete host and service downtimes') ); $this->providePermission( 'icingadb/command/process-check-result', $this->translate('Allow to process host and service check results') ); $this->providePermission( 'icingadb/command/feature/instance', $this->translate('Allow to toggle instance-wide features') ); $this->providePermission( 'icingadb/command/feature/object/*', $this->translate('Allow to toggle all features on host and service objects') ); $this->providePermission( 'icingadb/command/feature/object/active-checks', $this->translate('Allow to toggle active checks on host and service objects') ); $this->providePermission( 'icingadb/command/feature/object/passive-checks', $this->translate('Allow to toggle passive checks on host and service objects') ); $this->providePermission( 'icingadb/command/feature/object/notifications', $this->translate('Allow to toggle notifications on host and service objects') ); $this->providePermission( 'icingadb/command/feature/object/event-handler', $this->translate('Allow to toggle event handlers on host and service objects') ); $this->providePermission( 'icingadb/command/feature/object/flap-detection', $this->translate('Allow to toggle flap detection on host and service objects') ); $this->providePermission( 'icingadb/command/send-custom-notification', $this->translate('Allow to send custom notifications for hosts and services') ); $this->providePermission( 'icingadb/object/show-source', $this->translate('Allow to view an object\'s source data. (May contain sensitive data!)') ); $this->provideRestriction( 'icingadb/filter/objects', $this->translate('Restrict access to the Icinga objects that match the filter') ); $this->provideRestriction( 'icingadb/filter/hosts', $this->translate('Restrict access to the Icinga hosts and services that match the filter') ); $this->provideRestriction( 'icingadb/filter/services', $this->translate('Restrict access to the Icinga services that match the filter') ); $this->provideRestriction( 'icingadb/denylist/routes', $this->translate('Prevent access to routes that are part of the list') ); $this->provideRestriction( 'icingadb/denylist/variables', $this->translate('Hide custom variables of Icinga objects that are part of the list') ); $this->provideRestriction( 'icingadb/protect/variables', $this->translate('Obfuscate custom variable values of Icinga objects that are part of the list') ); if (! $this::exists('monitoring') || ($authenticated && ! $auth->getUser()->can('module/monitoring'))) { $routeDenylist = []; if ($authenticated && ! $auth->getUser()->isUnrestricted()) { // The empty array is for PHP pre 7.4, older versions require at least a single param for array_merge $routeDenylist = array_flip(array_merge([], ...array_map(function ($restriction) { return StringHelper::trimSplit($restriction); }, $auth->getRestrictions('icingadb/denylist/routes')))); } /* * Available navigation items */ $this->provideNavigationItem('icingadb-host-action', $this->translate('Host Action')); $this->provideNavigationItem('icingadb-service-action', $this->translate('Service Action')); /** * Search urls */ $this->provideSearchUrl( $this->translate('Tactical Overview'), 'icingadb/tactical', 100 ); $this->provideSearchUrl( $this->translate('Hosts'), 'icingadb/hosts?sort=host.state.severity&limit=10', 99 ); $this->provideSearchUrl( $this->translate('Services on Hosts'), 'icingadb/services?sort=service.state.severity&limit=10&_hostFilterOnly', 98 ); $this->provideSearchUrl( $this->translate('Services'), 'icingadb/services?sort=service.state.severity&limit=10', 97 ); if (! array_key_exists('hostgroups', $routeDenylist)) { $this->provideSearchUrl( $this->translate('Hostgroups'), 'icingadb/hostgroups?limit=10', 96 ); } if (! array_key_exists('servicegroups', $routeDenylist)) { $this->provideSearchUrl( $this->translate('Servicegroups'), 'icingadb/servicegroups?limit=10', 95 ); } /** * Current Incidents */ $dashboard = $this->dashboard(N_('Current Incidents'), ['priority' => 50]); $dashboard->add( N_('Service Problems'), 'icingadb/services?service.state.is_problem=y' . '&view=minimal&limit=32&sort=service.state.severity desc', 100 ); $dashboard->add( N_('Recently Recovered Services'), 'icingadb/services?service.state.soft_state=0' . '&view=minimal&limit=32&sort=service.state.last_state_change desc', 110 ); $dashboard->add( N_('Host Problems'), 'icingadb/hosts?host.state.is_problem=y' . '&view=minimal&limit=32&sort=host.state.severity desc', 120 ); /** * Overdue */ $dashboard = $this->dashboard(N_('Overdue'), ['priority' => 70]); $dashboard->add( N_('Late Host Check Results'), 'icingadb/hosts?host.state.is_overdue=y' . '&view=minimal&limit=15&sort=host.state.severity desc', 100 ); $dashboard->add( N_('Late Service Check Results'), 'icingadb/services?service.state.is_overdue=y' . '&view=minimal&limit=15&sort=service.state.severity desc', 110 ); $dashboard->add( N_('Acknowledgements Active For At Least Three Days'), 'icingadb/comments?comment.entry_type=ack&comment.entry_time<-3 days' . '&view=minimal&limit=15&sort=comment.entry_time', 120 ); $dashboard->add( N_('Downtimes Active For At Least Three Days'), 'icingadb/downtimes?downtime.is_in_effect=y&downtime.scheduled_start_time<-3 days' . '&view=minimal&limit=15&sort=downtime.start_time', 130 ); /** * Muted */ $dashboard = $this->dashboard(N_('Muted'), ['priority' => 80]); $dashboard->add( N_('Disabled Service Notifications'), 'icingadb/services?service.notifications_enabled=n' . '&view=minimal&limit=15&sort=service.state.severity desc', 100 ); $dashboard->add( N_('Disabled Host Notifications'), 'icingadb/hosts?host.notifications_enabled=n' . '&view=minimal&limit=15&sort=host.state.severity desc', 110 ); $dashboard->add( N_('Disabled Service Checks'), 'icingadb/services?service.active_checks_enabled=n' . '&view=minimal&limit=15&sort=service.state.last_state_change', 120 ); $dashboard->add( N_('Disabled Host Checks'), 'icingadb/hosts?host.active_checks_enabled=n' . '&view=minimal&limit=15&sort=host.state.last_state_change', 130 ); $dashboard->add( N_('Acknowledged Problem Services'), 'icingadb/services?service.state.is_acknowledged!=n&service.state.is_problem=y' . '&view=minimal&limit=15&sort=service.state.severity desc', 140 ); $dashboard->add( N_('Acknowledged Problem Hosts'), 'icingadb/hosts?host.state.is_acknowledged!=n&host.state.is_problem=y' . '&view=minimal&limit=15&sort=host.state.severity desc', 150 ); /** * @var \Icinga\Application\Modules\Module $this * * Problems section in case monitoring is disabled */ $problemSection = $this->menuSection(N_('Problems'), [ 'renderer' => array( 'TotalProblemsBadge', 'state' => 'critical' ), 'icon' => 'attention-circled', 'priority' => 20 ]); $problemSection->add(N_('Host Problems'), [ 'renderer' => (new HostProblemsBadge())->disableLink(), 'icon' => 'server', 'description' => $this->translate('List current host problems'), 'url' => 'icingadb/hosts?host.state.is_problem=y' . '&sort=host.state.severity desc', 'priority' => 50 ]); $problemSection->add(N_('Service Problems'), [ 'renderer' => (new ServiceProblemsBadge())->disableLink(), 'icon' => 'cog', 'description' => $this->translate('List current service problems'), 'url' => 'icingadb/services?service.state.is_problem=y' . '&sort=service.state.severity desc', 'priority' => 60 ]); $problemSection->add(N_('Service Grid'), [ 'icon' => 'cogs', 'description' => $this->translate('Display service problems as grid'), 'url' => 'icingadb/services/grid?problems', 'priority' => 70 ]); $problemSection->add(N_('Current Downtimes'), [ 'description' => $this->translate('List current downtimes'), 'url' => 'icingadb/downtimes?downtime.is_in_effect=y', 'priority' => 80, 'icon' => 'plug' ]); /** * @var \Icinga\Application\Modules\Module $this * * Overview section in case monitoring is disabled */ $overviewSection = $this->menuSection('Overview', [ 'icon' => 'binoculars', 'priority' => 30 ]); $overviewSection->add(N_('Tactical Overview'), [ 'url' => 'icingadb/tactical', 'description' => $this->translate('Open tactical overview'), 'priority' => 40, 'icon' => 'chart-pie' ]); $overviewSection->add(N_('Hosts'), [ 'priority' => 50, 'description' => $this->translate('List hosts'), 'url' => 'icingadb/hosts', 'icon' => 'server' ]); $overviewSection->add(N_('Services'), [ 'priority' => 60, 'description' => $this->translate('List services'), 'url' => 'icingadb/services', 'icon' => 'cog' ]); if (! array_key_exists('hostgroups', $routeDenylist)) { $overviewSection->add(N_('Host Groups'), [ 'description' => $this->translate('List host groups'), 'url' => 'icingadb/hostgroups', 'priority' => 70, 'icon' => 'network-wired' ]); } if (! array_key_exists('servicegroups', $routeDenylist)) { $overviewSection->add(N_('Service Groups'), [ 'description' => $this->translate('List service groups'), 'url' => 'icingadb/servicegroups', 'priority' => 80, 'icon' => 'cogs' ]); } if (! array_key_exists('contactgroups', $routeDenylist)) { $overviewSection->add(N_('Contact Groups'), [ 'description' => $this->translate('List contact groups'), 'url' => 'icingadb/contactgroups', 'priority' => 90, 'icon' => 'users' ]); } if (! array_key_exists('contacts', $routeDenylist)) { $overviewSection->add(N_('Contacts'), [ 'description' => $this->translate('List contacts'), 'url' => 'icingadb/contacts', 'priority' => 100, 'icon' => 'user-friends' ]); } $overviewSection->add(N_('Comments'), [ 'url' => 'icingadb/comments', 'description' => $this->translate('List comments'), 'priority' => 110, 'icon' => 'comments' ]); $overviewSection->add(N_('Downtimes'), [ 'url' => 'icingadb/downtimes', 'description' => $this->translate('List downtimes'), 'priority' => 120, 'icon' => 'plug' ]); /** * @var \Icinga\Application\Modules\Module $this * * History section in case monitoring is disabled */ $section = $this->menuSection(N_('History'), array( 'icon' => 'history', 'priority' => 90 )); $section->add(N_('Notifications'), array( 'icon' => 'bell', 'description' => $this->translate('List notifications'), 'priority' => 20, 'url' => 'icingadb/notifications', )); $section->add(N_('Event Overview'), array( 'icon' => 'history', 'description' => $this->translate('Open event overview'), 'priority' => 30, 'url' => 'icingadb/history' )); } else { /* * Available navigation items */ $this->provideNavigationItem( 'icingadb-host-action', $this->translate('Host Action') . ' (Icinga DB)' ); $this->provideNavigationItem( 'icingadb-service-action', $this->translate('Service Action') . ' (Icinga DB)' ); /** @var \Icinga\Application\Modules\Module $this */ $section = $this->menuSection('Icinga DB', [ 'icon' => 'database', 'priority' => 30 ]); $section->add(N_('Tactical Overview'), [ 'url' => 'icingadb/tactical', 'priority' => 10, 'description' => $this->translate('Open tactical overview'), 'icon' => 'chart-pie' ]); $section->add(N_('Hosts'), [ 'priority' => 20, 'description' => $this->translate('List hosts'), 'renderer' => 'HostProblemsBadge', 'url' => 'icingadb/hosts', 'icon' => 'server' ]); $section->add(N_('Services'), [ 'priority' => 30, 'description' => $this->translate('List services'), 'renderer' => 'ServiceProblemsBadge', 'url' => 'icingadb/services', 'icon' => 'cog' ]); $section->add(N_('Service Grid'), [ 'icon' => 'cog', 'description' => $this->translate('Display service problems as grid'), 'url' => 'icingadb/services/grid?problems', 'priority' => 40 ]); $routeDenylist = []; if ($authenticated && ! $auth->getUser()->isUnrestricted()) { // The empty array is for PHP pre 7.4, older versions require at least a single param for array_merge $routeDenylist = array_flip(array_merge([], ...array_map(function ($restriction) { return StringHelper::trimSplit($restriction); }, $auth->getRestrictions('icingadb/denylist/routes')))); } if (! array_key_exists('hostgroups', $routeDenylist)) { $section->add(N_('Host Groups'), [ 'url' => 'icingadb/hostgroups', 'priority' => 50, 'description' => $this->translate('List host groups'), 'icon' => 'network-wired' ]); } if (! array_key_exists('servicegroups', $routeDenylist)) { $section->add(N_('Service Groups'), [ 'url' => 'icingadb/servicegroups', 'priority' => 60, 'description' => $this->translate('List service groups'), 'icon' => 'cogs' ]); } if (! array_key_exists('contactgroups', $routeDenylist)) { $section->add(N_('Contact Groups'), [ 'url' => 'icingadb/contactgroups', 'priority' => 70, 'description' => $this->translate('List contact groups'), 'icon' => 'users' ]); } if (! array_key_exists('contacts', $routeDenylist)) { $section->add(N_('Contacts'), [ 'url' => 'icingadb/contacts', 'priority' => 80, 'description' => $this->translate('List contacts'), 'icon' => 'user-friends' ]); } $section->add(N_('Comments'), [ 'url' => 'icingadb/comments', 'priority' => 90, 'description' => $this->translate('List comments'), 'icon' => 'comments' ]); $section->add(N_('Downtimes'), [ 'url' => 'icingadb/downtimes', 'priority' => 100, 'description' => $this->translate('List downtimes'), 'icon' => 'plug' ]); $section->add(N_('Notifications'), [ 'url' => 'icingadb/notifications', 'priority' => 110, 'description' => $this->translate('List notifications'), 'icon' => 'bell' ]); $section->add(N_('History'), [ 'url' => 'icingadb/history', 'priority' => 120, 'description' => $this->translate('List history'), 'icon' => 'history' ]); } $this->provideConfigTab('database', [ 'label' => t('Database'), 'title' => t('Configure the database backend'), 'url' => 'config/database' ]); $this->provideConfigTab('redis', [ 'label' => t('Redis'), 'title' => t('Configure the Redis connections'), 'url' => 'config/redis' ]); $this->provideConfigTab('command-transports', [ 'label' => t('Command Transports'), 'title' => t('Configure command transports'), 'url' => 'command-transport' ]); $cssDirectory = $this->getCssDir(); $cssFiles = new RecursiveIteratorIterator(new RecursiveDirectoryIterator( $cssDirectory, RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS )); foreach ($cssFiles as $path) { $this->provideCssFile(ltrim(substr($path, strlen($cssDirectory)), DIRECTORY_SEPARATOR)); } $this->provideJsFile('action-list.js'); $this->provideJsFile('loadmore.js'); $this->provideJsFile('progress-bar.js'); } icingadb-web-1.4.0/doc/000077500000000000000000000000001516124654200146025ustar00rootroot00000000000000icingadb-web-1.4.0/doc/01-About.md000066400000000000000000000050061516124654200164150ustar00rootroot00000000000000# Icinga DB Web Icinga DB is a set of components for publishing, synchronizing and visualizing monitoring data in the Icinga ecosystem, consisting of: * Icinga DB Web which connects to both a Redis®[\*](TRADEMARKS.md#redis) server and a database to view and work with monitoring data * Icinga 2 with its [Icinga DB feature](https://icinga.com/docs/icinga-2/latest/doc/14-features/#icinga-db) enabled, responsible for publishing monitoring configuration, check results, states changes and history items to the Redis® server * And the [Icinga DB daemon](https://icinga.com/docs/icinga-db/latest/doc/01-About/), which synchronizes monitoring data between the Redis® server and the database ![Icinga DB Architecture](res/icingadb-architecture.png) ## Features Icinga DB Web offers a modern and streamlined design to provide a clear and concise overview of your monitoring environment, also with dark and light mode support. ![Icinga DB Dashboard](res/icingadb-dashboard.png) ### Various List Layouts The view switcher allows to control the level of detail displayed in host and service list views: ![View Switcher Preview](res/view-switcher-preview.png) ### Search with Autocomplete The search bar in list views can be used for everything from simple searches to creating complex filters. It allows full keyboard control and also supports contextual auto-completion. In addition, there is an editor for easier filter creation. ![Searchbar Completion Preview](res/searchbar-completion-preview.png) ### Clean Detail Views Host and service detail views are structured to make best use of available space. Related information is grouped and important information is at the top for instant access without having to scroll down. ![Service Detail Preview](res/service-detail-preview.png) ### Modal Dialogs Any interaction that requires user input, such as acknowledging problems, scheduling downtimes, etc., shows a modal dialog over the current view to preserve context and focus on interaction. ![Modal Dialog Preview](res/modal-dialog-preview.png) ### Bulk Operations Bulk interactions such as scheduling downtimes for multiple objects, acknowledging multiple problems, etc. are easily accomplished with the `Continue With` control that operates on filtered lists. ![Continue With Preview](res/continue-with-preview.png) ## Installation To install Icinga DB Web see [Installation](02-Installation.md). ## License Icinga DB Web and the Icinga DB Web documentation are licensed under the terms of the GNU General Public License Version 3. icingadb-web-1.4.0/doc/02-Installation.md000066400000000000000000000022111516124654200200000ustar00rootroot00000000000000 # Installing Icinga DB Web The recommended way to install Icinga DB Web is to use prebuilt packages for all supported platforms from our official release repository. Please follow the steps listed for your target operating system, which guide you through setting up the repository and installing Icinga DB Web. To upgrade an existing Icinga DB Web installation to a newer version, see the [Upgrading](05-Upgrading.md) documentation for the necessary steps. ![Icinga DB Web](res/icingadb-web.png) Before installing Icinga DB Web, make sure you have installed the [Icinga DB daemon](https://icinga.com/docs/icinga-db/latest/doc/02-Installation/). ## Installing the Package If the [repository](https://packages.icinga.com) is not configured yet, please add it first. Then use your distribution's package manager to install the `icingadb-web` package or install [from source](02-Installation.md.d/From-Source.md). This concludes the installation. Now proceed with the [configuration](03-Configuration.md). icingadb-web-1.4.0/doc/02-Installation.md.d/000077500000000000000000000000001516124654200203035ustar00rootroot00000000000000icingadb-web-1.4.0/doc/02-Installation.md.d/From-Source.md000066400000000000000000000014161516124654200227700ustar00rootroot00000000000000# Installing Icinga DB Web from Source Please see the Icinga Web documentation on [how to install modules](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation) from source. Make sure you use `icingadb` as the module name. The following requirements must also be met. ### Requirements * PHP ≥ 8.2 * MySQL or PostgreSQL PDO PHP libraries * The following PHP modules must be installed: `cURL`, `dom`, `json`, `libxml` * [Icinga DB](https://github.com/Icinga/icingadb) ≥ 1.4 * [Icinga Web 2](https://github.com/Icinga/icingaweb2) ≥ 2.12.5 * [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) ≥ 0.19.0 * [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) ≥ 0.15.0 icingadb-web-1.4.0/doc/03-Configuration.md000066400000000000000000000233441516124654200201610ustar00rootroot00000000000000# Configuration If Icinga Web has been installed but not yet set up, please visit Icinga Web and follow the web-based setup wizard. For Icinga Web setups already running, log in to Icinga Web with a privileged user and follow the steps below to configure Icinga DB Web: If you have previously used the monitoring module, there is an option to [migrate](10-Migration.md) some settings. ## Database Configuration Connection configuration for the database to which Icinga DB synchronizes monitoring data. 1. Create a new resource for the Icinga DB database via the `Configuration → Application → Resources` menu. 2. Configure the resource you just created as the database connection for the Icinga DB Web module using the `Configuration → Modules → icingadb → Database` menu. This configuration is stored in `/etc/icingaweb2/modules/icingadb/config.ini`. The `resource` option must match the name of the database resource you created for the Icinga DB database, for example: ``` [icingadb] resource = "icingadb" ``` If you use MySQL or MariaDB and notice poor performance for history queries, you can also disable the query optimizer for these queries in the same section: ``` [icingadb] ... disable_optimizer_for_history_queries = 1 ``` This adds hints to disable the optimizer for history queries and may improve performance on some MySQL/MariaDB versions. ## Redis® Configuration Connection configuration for the Redis® server where Icinga 2 writes check results. This data is used to display the latest state information in Icinga DB Web. 1. Configure the connection to the Redis® server through the `Configuration → Modules → icingadb → Redis` menu. !!! info If you are running a high-availability Icinga 2 setup, also configure the secondary master's Redis® connection details. Icinga DB Web then uses this connection if the primary one is not available. ## Command Transport Configuration In order to acknowledge problems, force checks, schedule downtimes, etc., Icinga DB Web needs access to the Icinga 2 API. For this you need an `ApiUser` object with at least the following permissions on the Icinga 2 side: * `actions/*` * `objects/query/*` * `objects/modify/*` * `status/query` !!! tip For single-node setups it is recommended to manage API credentials in the `/etc/icinga2/conf.d/api-users.conf` file. If you are running a high-availability Icinga 2 setup, please manage the credentials in the master zone. 1. Please add the following Icinga 2 configuration and change the password accordingly: ``` object ApiUser "icingadb-web" { password = "CHANGEME" permissions = [ "actions/*", "objects/modify/*", "objects/query/*", "status/query" ] } ``` 2. Restart Icinga 2 for these changes to take effect. 3. Then configure a command transport for Icinga DB Web using the credentials you just created via the `Configuration → Modules → icingadb → Command Transports` menu. !!! info If you are running a high-availability Icinga 2 setup, also configure the secondary master's API command transport. Icinga DB Web then uses this transport if the primary one is not available. ## General Configuration You can adjust some default values of options users have while interacting with particular dialogs in the UI. (e.g. While acknowledging a problem) These options can not be adjusted in the UI directly, but have to be set in the configuration file `/etc/icingaweb2/modules/icingadb/config.ini`. ### Available Settings and defaults | Option | Description | Default | |-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------| | acknowledge_comment_text | Sets the value for "Comment" in the Acknowledgement dialog | "" | | acknowledge_expire | Toggles "Use Expire Time" in the Acknowledgement dialog. | **0 (false)** | | acknowledge_expire_time | Sets the value for "Expire Time" in the Acknowledgement dialog. It is calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). | **1 hour (PT1H)**. | | acknowledge_notify | Toggles "Send Notification" in the Acknowledgement dialog. | **1 (true)** | | acknowledge_persistent | Toggles "Persistent Comment" in the Acknowledgement dialog. | **0 (false)** | | acknowledge_sticky | Toggles "Sticky Acknowledgement" in the Acknowledgement dialog. | **0 (false)** | | comment_expire | Toggles "Use Expire Time" in the Comment dialog. | **0 (false)** | | hostdowntime_comment_text | Sets the value for "Comment" in the Host Downtime dialog | "" | | servicedowntime_comment_text | Sets the value for "Comment" in the Service Downtime dialog. | "" | | comment_expire_time | Sets the value for "Expire Time" in the Comment dialog. It is calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). | **1 hour (PT1H)** | | custom_notification_forced | Toggles "Forced" in the Custom Notification dialog. | **0 (false)** | | hostdowntime_all_services | Toggles "All Services" in the Schedule Host Downtime dialog. | **0 (false)** | | hostdowntime_end_fixed | Sets the value for "End Time" in the Schedule Host Downtime dialog for a **Fixed** downtime. It is calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). | **1 hour (PT1H)**. | | hostdowntime_end_flexible | Sets the value for "End Time" in the Schedule Host Downtime dialog for a **Flexible** downtime. It is calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). | **2 hours (PT2H)**. | | hostdowntime_flexible_duration | Sets the value for "Flexible Duration" in the Schedule Host Downtime dialog for a **Flexible** downtime. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). | **2 hour (PT2H)**. | | servicedowntime_end_fixed | Sets the value for "End Time" in the Schedule Service Downtime dialog for a **Fixed** downtime. It is calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). | **1 hour (PT1H)**. | | servicedowntime_end_flexible | Sets the value for "End Time" in the Schedule Service Downtime dialog for a **Flexible** downtime. It is calculated as now + this setting. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). | **1 hour (PT1H)**. | | servicedowntime_flexible_duration | Sets the value for "Flexible Duration" in the Schedule Service Downtime dialog for a **Flexible** downtime. Format is a [PHP Dateinterval](http://www.php.net/manual/en/dateinterval.construct.php). | **2 hour (PT2H)**. | | plugin_output_character_limit | Sets the maximum number of characters to display in plugin output. | **10000** | ### Example Setting acknowledgements with 2 hours expire time by default. ``` [settings] acknowledge_expire = 1 acknowledge_expire_time = PT2H ``` ## Security To grant users permissions to run commands and restrict them to specific views, see the [Security](04-Security.md) documentation for the necessary steps. icingadb-web-1.4.0/doc/04-Security.md000066400000000000000000000176121516124654200171630ustar00rootroot00000000000000# Security Icinga DB Web allows users to send commands to Icinga. Users may be restricted to a specific set of commands, by use of **permissions**. The same applies to routes, objects and variables. To these, users can be restricted by use of **restrictions**. ## Permissions > **Restricted Command Permissions:** > > If a role [limits users](#filters) to a specific set of results, the command > permissions or refusals of the very same role only apply to these results. | Name | Allow... | |------------------------------------------------|----------------------------------------------------------------------------------| | icingadb/command/* | all commands | | icingadb/command/schedule-check | to schedule host and service checks | | icingadb/command/schedule-check/active-only | to schedule host and service checks (Only on objects with active checks enabled) | | icingadb/command/acknowledge-problem | to acknowledge host and service problems | | icingadb/command/remove-acknowledgement | to remove problem acknowledgements | | icingadb/command/comment/* | to add and delete host and service comments | | icingadb/command/comment/add | to add host and service comments | | icingadb/command/comment/delete | to delete host and service comments | | icingadb/command/downtime/* | to schedule and delete host and service downtimes | | icingadb/command/downtime/schedule | to schedule host and service downtimes | | icingadb/command/downtime/delete | to delete host and service downtimes | | icingadb/command/process-check-result | to process host and service check results | | icingadb/command/feature/instance | to toggle instance-wide features | | icingadb/command/feature/object/* | to toggle all features on host and service objects | | icingadb/command/feature/object/active-checks | to toggle active checks on host and service objects | | icingadb/command/feature/object/passive-checks | to toggle passive checks on host and service objects | | icingadb/command/feature/object/notifications | to toggle notifications on host and service objects | | icingadb/command/feature/object/event-handler | to toggle event handlers on host and service objects | | icingadb/command/feature/object/flap-detection | to toggle flap detection on host and service objects | | icingadb/command/send-custom-notification | to send custom notifications for hosts and services | | icingadb/object/show-source | to view an object's source data. (May contain sensitive data!) | ## Restrictions ### Filters Filters limit users to a specific set of results. > **Note:** > > Filters from multiple roles will widen available access. | Name | Description | |--------------------------|------------------------------------------------------------------------| | icingadb/filter/objects | Restrict access to the Icinga objects that match the filter | | icingadb/filter/hosts | Restrict access to the Icinga hosts and services that match the filter | | icingadb/filter/services | Restrict access to the Icinga services that match the filter | `icingadb/filter/objects` will only allow users to access matching objects. This applies to all objects, not just hosts or services. It should be one or more [filter expressions](#filter-expressions). `icingadb/filter/hosts` will only allow users to access matching hosts and services. Other objects remain unrestricted. It should be one or more [filter expressions](#filter-expressions). `icingadb/filter/services` will only allow users to access matching services. Other objects remain unrestricted. It should be one or more [filter expressions](#filter-expressions). ### Denylists Denylists prevent users from accessing information and in some cases will block them entirely from it. > **Note:** > > Denylists from multiple roles will further limit access. | Name | Description | |-----------------------------|-------------------------------------------------------------------| | icingadb/denylist/routes | Prevent access to routes that are part of the list | | icingadb/denylist/variables | Hide custom variables of Icinga objects that are part of the list | `icingadb/denylist/routes` will block users from accessing defined routes and from related information elsewhere. For example, if `hostgroups` are part of the list a user won't have access to the hostgroup overview nor to a host's groups shown in its detail area. This should be a comma-separated list. Possible values are: hostgroups, servicegroups, contacts, contactgroups `icingadb/denylist/variables` will block users from accessing certain custom variables. A user affected by this won't see that those variables even exist. This should be a comma-separated list of [variable paths](#variable-paths). It is possible to use [match patterns](#match-patterns). ### Protections Protections prevent users from accessing actual data. They will know that there is something, but not what exactly. > **Note:** > > Denylists from multiple roles will further limit access. | Name | Description | |----------------------------|------------------------------------------------------------------------------| | icingadb/protect/variables | Obfuscate custom variable values of Icinga objects that are part of the list | `icingadb/protect/variables` will replace certain custom variable values with `***`. A user affected by this will still be able to see the variable names, though. This should be a comma-separated list of [variable paths](#variable-paths). It is possible to use [match patterns](#match-patterns). ### Formats #### Filter Expressions These consist of one or more [filter expressions](https://icinga.com/docs/icinga-web-2/latest/doc/06-Security/#filter-expressions). Allowed columns are: * host.name * service.name * hostgroup.name * servicegroup.name * host.user.name * service.user.name * host.usergroup.name * service.usergroup.name * host.vars.[\](#variable-paths) * service.vars.[\](#variable-paths) #### Match Patterns Match patterns allow a very simple form of wildcard matching. Use `*` in any place to match zero or any characters. #### Variable Paths Icinga DB Web allows to express any custom variable depth in variable paths. So they may be not just names. Consider the following variables: ``` vars.os = "Linux" vars.team = { name = "Support", on-site = ["mo", "tue", "wed", "thu", "fr"] } ``` To reference `vars.os`, use `os`. To reference `vars.team.name`, use `team.name`. To reference `vars.team.on-site`, use `team.on-site[0]` for the first list position and `team.on-site[4]` for the last one. Combined with a [match pattern](#match-patterns) it is also possible to perform a *contains* check: `team.on-site[*]` icingadb-web-1.4.0/doc/05-Upgrading.md000066400000000000000000000145761516124654200173030ustar00rootroot00000000000000# Upgrading Icinga DB Web Specific version upgrades are described below. Please note that version upgrades are incremental. If you are upgrading across multiple versions, make sure to follow the steps for each of them. ## Upgrading to Icinga DB Web v1.4 **Behavior Changes** Modules that provide integrations for a host's or service's detail view now have unrestricted access to all custom variables of said object. This is the case by accessing custom variables via the object's `customvars` property. This was always meant to be the case, but due to an implementation oversight, the restrictions configured via `icingadb/filter/objects`, `icingadb/filter/hosts` and `icingadb/filter/services` were applied and caused unexpected or erroneous behavior. Though, in some cases this may not have been noticed or seen as a problem, so when upgrading we recommend to review modules that provide such integrations and check if they rely on the previous behavior. If so, accessing the `vars` property of an object should have the desired effect. The following modules made by Icinga are safe and do not need an update to still work: * Icinga Business Process Modeling * Icinga Web Graphite Integration Cases where this may not have been noticed are as follows: * Restrictions only reference `host.*` columns and integrations only require access to host variables. * Restrictions only reference `service.*` columns and integrations only require access to service variables. Since these are very specific cases, we don't expect broad expectation of the previous behavior, but please review your setup nonetheless if you use third party or custom modules that access custom variables this way. ## Upgrading to Icinga DB Web v1.3 **Requirements** * The minimum required Icinga Web version is now 2.12. **Breaking Changes** * The restrictions `icingadb/denylist/variables` and `icingadb/protect/variables` from different roles are now merged into a single list, respectively. This means that variables denied in one role will not show up anymore if another role denies access to different variables. The same applies to `icingadb/protect/variables`, in which case variables protected in one role will now be protected even if another role protects different variables. This has been done to simplify the configuration and to get it more in line with how refusals work in Icinga Web. * When using the `?columns` parameter to filter for specific columns and exporting to CSV/JSON, the exported file will only contain the listed columns of the Hosts/Services, if the parameter is not set, all columns will be included. * If a relation is entirely empty and would result in an empty JSON-object, the JSON-export will not create an object for it at all. (instead of `{"someKey":"someValue","emptyObject":{}}` we will now export `{"someKey":"someValue"}`) **Removed Features** * The routes `users`, `user`, `usergroup` and `usergroups` have been removed. * Accessing these routes will result in a 404 error. * The corresponding values for `icingadb/denylist/routes` have no effect anymore. * The alternatives (`contacts`, `contact` , `contactgroup` and `contactgroups`) have been available since v1.2. * The migration widget in the top right has been removed. If you have not adjusted your navigation items, dashboards and bookmarks to support the new filter syntax, you will need to do so manually now (see [Upgrading to Icinga DB Web v1.1](#upgrading-to-icinga-db-web-v11) for details). * Modules with support for IDO and Icinga DB will now default to use Icinga DB. * The accompanying endpoint `icingadb/migrate/monitoring-url` has been removed. * The class `Icinga\Module\Icingadb\Model\Behavior\BoolCast` has been removed. * Use `\ipl\Orm\Behavior\BoolCast` instead. ## Upgrading to Icinga DB Web v1.2 **Requirements** This version is released alongside Icinga DB 1.4.0 and Icinga 2.15.0. A change to the internal communication API requires these updates to be applied together. **Terminology Changes** Due to user feedback, we changed the terminology of _users_ and _user groups_ configured in Icinga. Both are now called _contacts_ and _contact groups_ respectively. **Behavior Changes** Macros referencing properties of type datetime are now rendered in ATOM format by default instead of unix timestamps. (e.g. `$host.state.last_state_change$`) **Deprecations** The following classes have been deprecated and will be removed in a future release: * `\Icinga\Module\Icingadb\Command\Object\PropagateHostDowntimeCommand` * `\Icinga\Module\Icingadb\Command\Object\ScheduleHostDowntimeCommand` * `\Icinga\Module\Icingadb\Command\Object\ScheduleServiceDowntimeCommand` The following methods have been deprecated and will be removed in a future release: * `\Icinga\Module\Icingadb\Common\IcingaRedis::instance()`: Use `\Icinga\Module\Icingadb\Common\Backend::getRedis()` instead. ## Upgrading to Icinga DB Web v1.1 **Breaking Changes** We've extended our filter syntax to include new signs for *like* and *unlike* comparisons. These are `~` and `!~`, respectively. The *equal* (`=`) and *unequal* (`!=`) operators won't perform any wildcard matching anymore due to this. If you have dashboards, navigation items or bookmarks that attempt to perform wildcard matching with *equal*/ *unequal* comparisons, the migration widget in the top right will toggle and suggest you an automatically transformed alternative. Please note that due to our release process, this change already affects installations of Icinga DB Web v1.0.x. The module's migration command already performs the transformations necessary once you migrate navigation items, dashboards and roles. (As described in the [Migration](10-Migration.md) chapter.) If you already migrated such manually in the past, and you don't want to perform the entire migration again, you can use the following command to only transform filters of such: `icingacli icingadb migrate filter [--no-backup]` By default, this creates backups of menu items, dashboards and roles. Pass `--no-backup` to disable this. ## Upgrading to Icinga DB Web v1.0 **Requirements** * You need at least Icinga DB version 1.0.0 to run Icinga DB Web v1.0.0. **Configuration Changes** * The restrictions `icingadb/blacklist/routes` and `icingadb/blacklist/variables` have been renamed to `icingadb/denylist/routes` and `icingadb/denylist/variables` respectively. If you use this restriction, make sure to adjust `/etc/icingaweb2/roles.ini` accordingly. icingadb-web-1.4.0/doc/09-Automation.md000066400000000000000000000220211516124654200174670ustar00rootroot00000000000000# Automation It is possible to issue command actions without a browser. To do so, a form needs to be submitted by a tool such as curl. This is also used in the example below. ## Request Format The request is required to be an Icinga Web API request. For this it is necessary to transmit the `Accept` HTTP header and set it to `application/json`. In addition to this, the request must be authenticated using the `Basic` schema. The HTTP request method is `POST` if not documented otherwise and it might be necessary to manually configure this (e.g. `-X POST` for curl). All endpoints support filters. To issue commands only for specific items, define a filter in the request's query string. If this filter is omitted, all items are affected. The options need to be transmitted in the request body as `multipart/form-data`. ## Response Format If the request succeeds, the HTTP response code is `200` and the response body contains a JSON object such as this: ```json { "status": "success", "data": [ { "type": "success", "message": "Added comment successfully" } ] } ``` If there's something wrong with the options, the HTTP response code is `422` and the response body contains a JSON object such as this: ```json { "status": "fail", "data": { "comment": [], "expire": [], "expire_time": ["The expire time must not be in the past"] } } ``` ## Example ```shell USER="icingaadmin" PASSWORD="icinga" BASEURL="http://localhost/icingaweb2" FILTER="host.name=docker-master" curl -H "Accept: application/json" -u $USER:$PASSWORD "$BASEURL/icingadb/hosts/add-comment?$FILTER" \ -F "comment=kaput" -F "expire_time=2023-10-05T20:00:00" -F "expire=y" ``` ```shell USER="icingaadmin" PASSWORD="icinga" BASEURL="http://localhost/icingaweb2" FILTER="host.name=docker-master" curl -H "Accept: application/json" -u $USER:$PASSWORD -X POST "$BASEURL/icingadb/hosts/delete-comment?$FILTER" ``` ## Option Types ### Text A simple text message. May contain newlines. ### Number An integer value. ### BoolEnum A string with the value of `y` for `true` and `n` for `false`. ### DateTime A date time string in the following format: `Y-m-d\TH:i:s` The timezone this is interpreted in depends on the user who's transmitting the request. To change this, log in as this user and change its preference setting. ### State | Value | Description | Applicable To | |-------|----------------|----------------| | 0 | UP / OK | Host / Service | | 1 | DOWN / WARNING | Host / Service | | 2 | CRITICAL | Service | | 3 | UNKNOWN | Service | ### PerfData Please have a look at the [Monitoring Plugins Development Guidelines](https://www.monitoring-plugins.org/doc/guidelines.html#AEN201). ### ChildOption | Value | Description | |-------|-----------------------------------------------------| | 0 | Do nothing with child hosts | | 1 | Schedule triggered downtime for all child hosts | | 2 | Schedule non-triggered downtime for all child hosts | ## Endpoints !!! info Some of the endpoints support configuration options. These are explained in the [Configuration](03-Configuration.md#available-settings-and-defaults) section. ### Acknowledge Problem #### Routes * icingadb/hosts/acknowledge * icingadb/services/acknowledge #### Options | Option | Required | Type | Depends On | Default | Configuration Option | |-------------|----------|----------|------------|--------------|--------------------------| | comment | y | Text | - | - | acknowledge_comment_text | | persistent | n | BoolEnum | - | n | acknowledge_persistent | | notify | n | BoolEnum | - | y | acknowledge_notify | | sticky | n | BoolEnum | - | n | acknowledge_sticky | | expire | n | BoolEnum | - | n | acknowledge_expire | | expire_time | y | DateTime | expire | now + 1 hour | acknowledge_expire_time | ### Add Comment #### Routes * icingadb/hosts/add-comment * icingadb/services/add-comment #### Options | Option | Required | Type | Depends On | Default | Configuration Option | |-------------|----------|----------|------------|--------------|----------------------| | comment | y | Text | - | - | - | | expire | n | BoolEnum | - | n | comment_expire | | expire_time | y | DateTime | expire | now + 1 hour | comment_expire_time | ### Delete Comments #### Routes * icingadb/comments/delete #### Options None. ### Check Now #### Routes * icingadb/hosts/check-now * icingadb/services/check-now #### Options None. ### Process Check Result #### Routes * icingadb/hosts/process-checkresult * icingadb/services/process-checkresult #### Options | Option | Required | Type | |----------|----------|----------| | status | y | State | | output | y | Text | | perfdata | n | PerfData | ### Remove Acknowledgement #### Routes * icingadb/hosts/remove-acknowledgement * icingadb/services/remove-acknowledgement #### Options None. ### Schedule Check #### Routes * icingadb/hosts/schedule-check * icingadb/services/schedule-check #### Options | Option | Required | Type | Default | |-------------|----------|----------|--------------| | check_time | y | DateTime | now + 1 hour | | force_check | n | BoolEnum | - | ### Schedule Host Downtime #### Routes * icingadb/hosts/schedule-downtime #### Options | Option | Required | Type | Depends On | Default | Configuration Option | |---------------|----------|-------------|------------|------------------------------------------------|---------------------------------------------------| | comment | y | Text | - | - | hostdowntime_comment_text | | start | y | DateTime | - | now | - | | end | y | DateTime | - | now + 1 hour (fixed), now + 2 hours (flexible) | hostdowntime_end_fixed, hostdowntime_end_flexible | | flexible | n | BoolEnum | - | n | - | | hours | y | Number | flexible | 2 | hostdowntime_flexible_duration | | minutes | y | Number | flexible | 0 | hostdowntime_flexible_duration | | all_services | n | BoolEnum | - | n | hostdowntime_all_services | | child_options | n | ChildOption | - | - | - | ### Schedule Service Downtime #### Routes * icingadb/services/schedule-downtime #### Options | Option | Required | Type | Depends On | Default | Configuration Option | |----------|----------|-------------|------------|------------------------------------------------|---------------------------------------------------------| | comment | y | Text | - | - | servicedowntime_comment_text | | start | y | DateTime | - | now | - | | end | y | DateTime | - | now + 1 hour (fixed), now + 2 hours (flexible) | servicedowntime_end_fixed, servicedowntime_end_flexible | | flexible | n | BoolEnum | - | n | - | | hours | y | Number | flexible | 2 | servicedowntime_flexible_duration | | minutes | y | Number | flexible | 0 | servicedowntime_flexible_duration | ### Delete Downtimes #### Routes * icingadb/downtimes/delete #### Options None. ### Send Custom Notification #### Routes * icingadb/hosts/send-custom-notification * icingadb/services/send-custom-notification #### Options | Option | Required | Type | Default | Configuration Option | |---------|----------|----------|---------|----------------------------| | comment | y | Text | - | - | | forced | n | BoolEnum | n | custom_notification_forced | icingadb-web-1.4.0/doc/10-Migration.md000066400000000000000000000161021516124654200172730ustar00rootroot00000000000000# Migration If you previously used the monitoring module, (built into Icinga Web 2) you may want to migrate your existing configuration, custom dashboards and navigation items as well as permissions or restrictions. If that is the case, this chapter has you covered. ## Configuration ### General configuration via config.ini Icinga DB Web still uses the same configuration format as the monitoring module. This means that the file `/etc/icingaweb2/modules/monitoring/config.ini` can simply be copied over to `/etc/icingaweb2/modules/icingadb/config.ini`: ``` cp /etc/icingaweb2/modules/monitoring/config.ini /etc/icingaweb2/modules/icingadb/config.ini ``` The behaviour of those options remains the same. ### Command Transports Icinga DB Web still uses the same configuration format for command transports. This means that the file `/etc/icingaweb2/modules/monitoring/commandtransports.ini` can simply be copied over to `/etc/icingaweb2/modules/icingadb/commandtransports.ini`. But note that Icinga DB Web doesn't support the commandfile (local and remote) anymore. Remove all sections that do **not** define `transport=api`. ### Protected Customvars The rules previously configured at `Configuration -> Modules -> monitoring -> Security` have moved into the roles configuration as a new restriction. This is called `icingadb/protect/variables` and accepts the same rules. Just copy them over. ## Navigation The monitoring module provides two custom navigation item types: `host-action` and `service-action` Icinga DB Web does the same, though uses different type names to achieve that: `icingadb-host-action` and `icingadb-service-action` With Icinga DB Web 1.1, its migrate command allows you to migrate these navigation items automatically: `icingacli icingadb migrate navigation --user= [--no-backup] [--override]` By default, this only migrates navigation items of specific users and keeps the old ones. The `--user` switch expects a username, with optional wildcards (`*`) to match multiple users. `--user=*` matches all users. Pass `--no-backup` to fully remove the old monitoring navigation items. A similar version of this command has already been available since Icinga Web 2.9.4. Due to this, the new command allows you to perform the migration from scratch again with the `--override` switch. (Provided you still have the old navigation items.) Otherwise, already migrated items are ignored. That's also a difference to the previous command, which duplicated items instead. ## Dashboards The dashboard item configuration does not change since it is related to Icinga Web. However, items that reference views of the monitoring module should be changed in order to permanently reference views of Icinga DB Web. With Icinga DB Web 1.1, its migrate command allows you to migrate such dashboard items automatically: `icingacli icingadb migrate dashboard --user= [--no-backup]` By default, this only migrates dashboards of specific users and creates backups. The `--user` switch expects a username, with optional wildcards (`*`) to match multiple users. `--user=*` matches all users. Pass `--no-backup` to disable backup creation. Please note, if you do so, that this makes resetting changes more difficult. ## Views and Exports ### Url Parameter `addColumns` The host and service list of the monitoring module allows to show/export additional information per object by using the URL parameter `addColumns`. Icinga DB Web has a very similar but much enhanced parameter: `columns` If you pass this to the host and service list of Icinga DB Web, you'll get an entirely different view mode in which you have full control over the information displayed. The parameter accepts a comma separated list of columns. This list also defines the order in which the columns are shown. As of now, there is no dedicated control in the UI to conveniently choose those columns. You can use all columns however, which are valid in the search bar as well. The migration widget, that's shown if you have access to monitoring and Icinga DB Web, also assists you by providing an example set of columns conveying the same information shown in the monitoring module lists. ## Access Control ### `monitoring/filter/objects` This is now `icingadb/filter/objects` but still accepts the same filter syntax. Only the columns have changed or support for them has been dropped. Check the table below for details: | Old Column Name | New Column Name | |----------------------|------------------------| | instance\_name | - | | host\_name | host.name | | hostgroup\_name | hostgroup.name | | service\_description | service.name | | servicegroup\_name | servicegroup.name | | \_host\_customvar | host.vars.customvar | | \_service\_customvar | service.vars.customvar | ### `monitoring/blacklist/properties` This is now `icingadb/denylist/variables`. However, it does not accept the same rules as `monitoring/blacklist/properties`. It still accepts a comma separated list of GLOB like filters, but with some features removed: * No distinction between host and service variables (`host.vars.` and `service.vars.` prefixes are no longer keywords) * No `**` to cross multiple level boundaries at once (`a.**.d` does not differ from `a.*.d`) * Dots are not significant (`foo.*.oof` and `foo*oof` will both match `foo.bar.oof`) Check the [security chapter](04-Security.md#variable-paths) for more details. ### Permissions The command permissions have not changed. It is only the module identifier that has changed of course: `monitoring/command/*` is now `icingadb/command/*` The `no-monitoring/contacts` permission (or *fake refusal*) is now a restriction: `icingadb/denylist/routes`. Add `contacts,contactgroups` to it to achieve the same effect. ### Perform The Migration To apply the necessary changes automatically, Icinga DB Web 1.1 provides this command: `icingacli icingadb migrate role [--role=] [--group=] [--override] [--no-backup]` By default, this only migrates roles with matching names or matching groups, doesn't change roles that were already manually migrated and creates backups. Either `--role` or `--group` must be passed, but not both. Both accept wildcards and just `*` matches all roles. Pass `--override` to forcefully update roles that appear to be already migrated. Please note that this will reset changes made to Icinga DB Web's rules, which were not equally applied to their monitoring module counterparts. Pass `--no-backup` to disable backup creation. Please note, if you do so, that this makes resetting changes more difficult. With respect to permissions, the command will only migrate the command permissions. If a role grants full or general access to the monitoring module, this is not automatically migrated. You have to adjust this manually. It gives you the chance to review the performed changes, before letting them loose on your users. Please also take in mind, that Icinga DB Web handles permissions and restrictions differently. Our blog provides details on that: https://icinga.com/blog/2021/04/07/web-access-control-redefined/#icingadb-permission-linkage icingadb-web-1.4.0/doc/11-Concepts.md000066400000000000000000000112001516124654200171130ustar00rootroot00000000000000# Views ## List Items Each list that is shown in the web interface consists of list items that follow this basic schema: ![DefaultAnatomy](res/ListItemAnatomy.jpg "A list item stripped down to the skeleton") ### Visual Each element has a so-called visual. It's purpose is to highlight certain elements in long lists and give an intuitive overview about the underlying objects' condition. In the case of host or service lists, the state is displayed. This makes it obvious if and where there are problems that need to be taken care of, immediately from the overview. ### Title The title briefly describes the state of the list element as an addition to the visual. For example, it contains the information that a host is currently DOWN state. While the visual gives an intuitive impression, the title explains what exactly happened. ### Meta The meta area provides additional details, usually displays of time. In the host and service lists, you can see how long the object has been in its current state. ### Caption The caption area contains detailed information about the list item. In the case of hosts and services this would be the check output. For comments and downtimes the comment texts of the user are displayed here. To make the default list view compact and consistent, long texts are truncated to a maximum of two lines. ## Different detail degrees The level of detail of the host and service lists is selectable - the above image shows the default list item, but there are two more iterations: In the compact mode the entire list item is displayed in one single line. Given, that there is enough space, the caption is placed in the same line as the rest and trimmed to fit. ![CompactAnatomy](res/ListItemAnatomyCompact.jpg "A more compact list item that fits in one line") The detailed mode displays the entire caption, which makes it possible to react to problems directly from the list view. ![DetailedAnatomy](res/ListItemAnatomyDetailed.jpg "A more detailed list item that spans four lines") ## Overdue Items In host and service lists overdue checks are highlighted. This makes it immediately apparent which objects may no longer be up-to-date. The red badge contains information about how long the check has been overdue. ![OverdueListAnatomy](res/ListAnatomyOverdue.jpg "A list with a highlighted item") ## Downtimes Downtime lists have an extra information to be displayed: the progress. How far along the downtime is, is immeadiately visible via the progress bar at the top of each list item. In the visual it is displayed how much time is left until the downtime ends. ![DowntimeListItem](res/ListItemDowntimeAnatomy.jpg "A downtime list item with the added progress bar in the top") # Modal The modal element introduces short interaction dialogues. For actions in the detail area, a modal dialog will appear. This preserves the left list column and thus the content better. ![ModalAnatomy](res/ModalAnatomy.jpg "A modal presenting a form") ### Utility The utility or close button is one of three ways to close the modal without confirming your changes. It's also possible to just click outside it's borders or press the escape key to return to the previous page. ### Title The title specifies the target, e.g. an action like: Acknowledge Problem, Add Comment or Schedule Downtime. ### Hint The hint explains more about how to interact with the modal and what is asked of you. It goes into detail about the consequences from an action to your environment ### Form The form is the main focus of the modal. It takes the call to action out of the normal page flow and makes it easy to focus on the task at hand. ### Submit With the submit button you apply the selections you made in the form and return to where you left off. # Details ## Check statistics Information about check execution is available in a handy graph that makes it possible to get all of the information at a glance. ![CheckStatistics](res/CheckStatisticsAnatomy.jpg "A panel with the statistics about check execution") ### Descriptions and Values Instead of having a table with the different check information there is a header that lays out the information in an intuitive way, giving weight to more important bits of data and their values. ### Graph The graphical element makes it easier to see, whether the check is on time or late. It's easy to immediately see the configured check interval and have a better grasp on how up-to-date the current state is. ### Indicator The indicator visualises where the current time fits in for orientation. ### Specifics The specifics put the exact times of the execution points in relation to each other. There are both the full timestamps and the passed time immediately visible. icingadb-web-1.4.0/doc/TRADEMARKS.md000066400000000000000000000013451516124654200165640ustar00rootroot00000000000000# Third-party Trademarks All trademarks, logos, and brand names are the property of their respective owners. Any mention of company, product, or service names in our documentations, product descriptions, or websites is solely for identification purposes. The use of these names, trademarks, and brands does not imply endorsement. This document acknowledges trademarks of companies and products, which are the property of their respective owners, whether registered or unregistered. ## Redis® Redis is a registered trademark of Redis Ltd. Any rights therein are reserved to Redis Ltd. Any use by Icinga GmbH is for referential purposes only and does not indicate any sponsorship, endorsement or affiliation between Redis and Icinga GmbH. icingadb-web-1.4.0/doc/res/000077500000000000000000000000001516124654200153735ustar00rootroot00000000000000icingadb-web-1.4.0/doc/res/CheckStatisticsAnatomy.jpg000066400000000000000000002353451516124654200225320ustar00rootroot00000000000000JFIFHH@ExifMM*i'+8Photoshop 3.08BIM8BIM%ُ B~+' }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzCC ?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( Wz`P__YC@PM"o Ywm܌" MKN.,o aL l.!ʆ -8 P@P@y>.)o;%=Y/h0~Sƚy Xkֽ+ZDm4Ȯl-QY@UP2YIb<ſtC]K+_M'W5M_naE kMJdY,; fG*( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (֧ز%cf.gPpYY s@UC+A  #Gr(dW"]F;xyn% bAn K j z@ HIe"$i%FT8K<;3@WY]F+Wm;s ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( (;O7>Pkmi~ ?mz0j <1CMk6߆呼k $ʀz &~K_x#>-1]xK沾P]jjiCV=p ž3k+ĭ:5|$дk>DPgkK_^džRtFho/,mmto@=C2~oΛ?Z>5:Rxgv ݵm5/u7ZѠ5mV2SMikO-/G |C~ZomYោV=.߷z0iǎ_Fohg7zk [뚲iw7V0j˨v,eq'|0YV|5#SK{|jY76|@N?d۶Wо xZޅ`Ω*gƱQko.cmo}ޙowy˦-6q^tv?`Ҿ0꺆CH/,cOC^o ? kUm3RdhV6yAo&o wF런?&֙;Q6y;Gֵd?qg:lQK v?iiݷx{Mm/j.u~:svƏ{67}v:jix{8 cF἞.6g'{?5eYKZkxFM.o,lYsl(hশ_{?kx/_ Ӽ9j nu/ ˮY@lM\A}5S?~&.~7Mĺ嬑h:lDž5}O(t)Bh(6v54xw^|~QMGJO4toGZ^y7V%3jz0O8# ~)TM3Q}2? <hF..,x— MK~a|W:eΡb]??px;|z׌-,dsZ&mD-';~nJOt`m_-|hZa06iA~srW@\i:N>jmbTm>_WX׵O_t\YKKn#'@-Q#cܨ7HwtY.c=JHip_$Ǵ_^ǂ%Pk y͏Z&xih}̪=??k?σkZAh߂j:$RKdW2/gN zx~ҧ2gh=/k;eK o,p>0k&7kme9eSHԕ/4oÚo\^{Ě ZCwm7,Mk{k=_V~2ij?_u71:o4.jZgʶʢFAJ)# ^#FOih X:Tu]͵07@c/)ov'~A[᎟~%>5ņjڮ{Zfi u'l'2[cbhG _<}O~77xn$4({hF }˧OR?m^1p5|]=b] 7_٣>g MU,Jլfu#k{5ޟ[ڀ}o@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( (;O7>Pvuu4VҧD KIfY QFH쨈>;w>|kZeYx# z?O<ʩo5_Zr[nJw ko_?h#=վ[_<3Kᮥ^'յ]cGOKk>)ү4WOI{6}5泵__n Ͻ4ٛ·5+߅PxW^?UΏ[ۙ5M}oLt5)5eofp ȿQ/'W :h)Fex4񷈬to~1OV#S'H=[Ob:]3jc߮^~~,n[xEx'z~y᫏jROFOo&_.a 혵M@7> k_H_DҬQC_iO2ǎ0?[ԭtm=t;}>+K"ĉ@M/xOM2xczw;aou-hmj7ĶWW2 xc$./{ٿWu\xs0wy$ 뺾vmo%2_?m=xiNy #t=j i>Uu`쭵xbE5C3^p?O -"m4i_ rw[IկK+w"M B~Ο'ߴ <ľCÚOl9k:-}?[-5jX-Z_t~~8[cM𿁿4h,47I߈mivCkaiz&[CP"s@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@xg7OGg";??|K(.|zVi=|~=3[e=m=cNQu? [Ew0V|yy䖰 ծ| qoM<ʢ&2}Q7nCҿUhAI]7Ğ%C\|(}KDաg]o:n$zΏiyǺ,ǟR,4k>2 ;_/SGֿf^7zvL+jSOvwmii]C񮿧[Uyl/-n&_#vzJk[NL -b9N]\ƿ-VLh~π N?jO$2}HRq@ҧz_%Fӵѓc9 Scyiy]k&Tj|?^3~X~ k_dxWl;}sD>$j.e*q5Q i)?>-xd>ՓO|TϏ<Au9|cci:Mk귶:}n?R~^8f_t~6!9 8xDӴ3I4N{k;MuW[& QP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( (,;ğu/?o' 7 G_=+֜:S5(cЮ^˧o%;y $37ߎ_Q ]|2M3U_㟊tSM4:~+x F,Q"i گ'޳:~ 7?&7#Ѣ];LѴf/xWLQtojw{McT[$Oq#Edxݭ{)uF=_U48eo|#/ mKGҼGo:No-KoG`_m_ y%sFV;^?[[n]moM}mg?Jv Ms$Z_κ{}zz}GZ N¾"m U=CM|v{\k?hK]hWIoM-wTσl+zoxJHAoi@ER$|[V1h |ּu?5Ų]֚L%T״)kYWt ]ZX}kzwn'ƿwcK>/EhsC AhXzٖVpt_K%؏~4Vx[恮i^{U5/6+_?m i ajNcHn.?~ݟgω_{Ú}Z|Gik1x~oះ/Q__򉞀?*฾~(^dφ~${;?u4˄ԓEw}pcN֗ 9)`>9&߃n |Eq-Kznjn|af_Mm>>>ӴK-F fVk}p ǿNk/'º 񮡧EL|kGNk4ωFO_\[ٰO/',|47υ>9߂F>λ|R4Եyg];NE{+38~ֿ?k SO$_V[m#JWj3]xK֭.?Sz_m.--̠%C -d aKޑ+.su4~(t]v}.m'AaoixᯈU4:%W~Zv}//C|0O5IoG։iwZ-߈ouxMQ$tSͥCo8 ~⟅'o: Vw_<,˪[Ɠ<K6 y|amƏ<[.E>E B˥_&E$ס4 ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( Mo x[ş6[ k)Ūϧx٭tFPP?9D>BgğU/_j lηv$hд^T?Ά?b#ߍ~?'[L4G4#ԼM+O Mhº^-.-ƞ"++TSMpg/#_?~|[+_ $|G_g|-]S^xhƏiYk$C\ŽCcKqٟ\-M&/٧Ǐj|r j,4[r^"::g^k #^|1GGӬ/O ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (/OOo_#<QuASO ͠ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (~2{)oV?O$?ٿ?~W1=G6T_(?GQH/z?iE@m#Q=G6T_(?GQH/z?iE@m#Q=G6T_(?+J[omo4ih41eb8VYDR̠@~ʟfI߉,3x ( ( ( ( ( ( ( ( ( ( ( ( ( ( Lv}O#_ڟ}G]kOڿy/O/'|ۨ¿iE@m#Q=G6T_(?GQH/z?iE@m#Q> Q>~6h6O|Fݼ1xsOWg/-$k6X!gam} t]A/@?N9hdIbXHE FJ:JA @ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( #oj_ޟ>h k:Hla6POYm}5` iVi? xx7O~7Er$2Ym/6ctٷx~ŕlo> ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (.| ǚ`γdzƎz֋vl3#mmOe{͍ʹ~.1Fjj_ u9/fIFey^q+m{c,6vq˙"WwZurG,i,NE*,HnGG\ FA@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( (ڳS|k|uz|@9-.bű4IZlou[oe䶹;/OҾ=iMoƿ lm汚i%w΋iyby:0{7/ _]}@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@Y灾9St{^:H֋vm5 C#Jջc}͍ͼ?/6Gɨ|3d1| 2Ⱥm%HRi%6=k{wRE xNNl?WTI#u9^9˕ee!hP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P( ( ( ( ( ( ( ( ( ( ( ( (?jKSƧeeoPEaDu'|Z dZF\].K{,j3ƛ}?Xlh [ n&엚΋gxEдCw7-nY^݀}@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@Uׁ:R4w^?O htִ[V6Fۼ7\@1x;I_Û0 0t4*kS4gѠ2Am473Is໩" \麕:H$lцU*g4( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (>?e-Kz -BO+|m[cu9-" Z .-Ɩ~_nNG>64KgO^-2KKgEttMԬ?VՕ]]C+ 22 r8#uP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ?uj6?_~^ 1jv#lZf+u e7wn|?HK?e/ڷOx7V#%b[t~yh{nVnGRЯȜi-P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@~ xǁuOM7fn#M_úFkN{r쮌屽{Kb`w"fl ?z4iCJzVy;9G -s˧I/>JꬌXVR FAg4P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( (ڷQcgjz|x;E꒶e-;Q g{fAĺDPI)~oK࿎> xI{&'o5m—t]wmiypUP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@yƿ]C:iqJm49n΍t 7& m}l^5F|/ǖ?'1~n<rGQ6%X,Rã^@nk M:K4>ywd\$I:մ7]OfP( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (~7|7͇m6nt^c]_zFmh.üоkV)mtlً3Fr Az]&]$J:^}p\ţZHf}G2%u<X2AAA y ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( (>e ψ? _^ 0FNiZ xTL/Zڱе64`]g:/㧃^0}2iϨɧI[gE"N6CѮ[kyR8P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@>o 2 w\ͲF5 k %\:.xlkV9!|;eo~AvEΐ$[}?KtVw9&&u Ad P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( Wa'frBIi !"Sع%ƀ9Sd҉ǸM@,p ??4OO @,p ??4OO @,p ??4OO @,p ??4OO @,p ??4OO @,p ??4OO @i~烞 +_u_ENj}3VP4RZ3hinhe5~XWto ~*jI`gI%,ˍOJi$Jilp?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''X? BhP?`''%CM,M/e$3P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@>?_#LnuAxTVKNv][9=yԩV@>|`_'I%-Kw]旼Aaa}>q˛\K~uh/@P@P@P@P@P@P@P@P@P@P@9YљYX2YNC)!Azo[QҬdR8X:PP@P@P@P@P@P( ( ( ( ( ( ( 6Ie (^ݶ>WuҿϮ@,}s}t?c='P \]+:X_wfFirş[yH -ʒbpjZ͢HY "8 ,2 TkA3#4 ( ( =Ÿ(b5{)|H+;Rh5_麕xZ|-{ ߳%X/|A?^Z×6u3`|WKFѺ2鲟1/캿8mx4dJٖaɍ6g8ʫ)V` X_Ϯ@,}s}t?c='P \]+:X_w?J{N>>WuҿϮ@,}s}t?c='P []+]uoiḙ խy .fhO:7o1^\g,@=(@ r<:hh ( ( (7UլkF4@Y I IeEsPMyčA?c?(琎pI[eXFՀ+?J{N>>WuҿϮ@,}s}t?c='P \]+:X_w?J{N>>WuҿϮ@,}s}t?c='P \]+:X_w?J{N>>WuҿϮ@#UW{>X.`r=.?<_׭[R,[EŬa-8xdpqTpQ@7( ( ( (^ݶuGA~g7֗Z|;mEnLw z%4yq߷ؖ[pcߋN>>WuҿϮ@,}s}t?c='P \]+:X_w?J{N>>Wuj٥X[(㉈x<9,V]}܀(!+ XfE9WFVSAP@P@P@VNQ NIª2c׿.4>L4p ODy ?ҿϮ@,}s}t?c='P \]+:X_w?J{N>>Wuҿ'P Z-7fX}vrʪҁ A #ր (lutP@P@P@P@P@P@( ( ( ( ( ( ( 6Ie (ķiu  #K2)@4P@P@Pq -մ |&KFP}$aހ=€ ( ( (>&>43{A4^QbUƉ!.$|yđ>@=P@P@P@P@P@NK-Nh؅K)TtxEp2H=дP@P@P@[4 RJq%]} ΀ (>&|4? HQ>ߣ_ڗʜcØ/_Iİ|>-|y}`_ʳٶ [&چ!KP@P@P|8k 'bem#!wѷb#-2N2( ( ( ( ( (71m%ֿ$`7/$b"jZ~>[  Ӿ3 :ۭՠmnP@P@ KMNOm d$p2QO 6@P@P@PHu >ʖ$ye `yI7 ( ( /[IQwyqsx&=ۃ b:APm@PxGE?:J( ( ( ( ( (( ( ( ( ( ( ( 6Ie (+׼$uA$bc+yb69$[I ?,RY@E参|O^??@ Ow%Ow%Ow%Ow%Ow%Ow%Ow%Ow%oiO$0Hqbqc+hh`f\2yF3$yqbYv ( ( (,Qʋ$RG$nWGYXe$"G3ˣŻViw)PsfG G*mSg-Ηj:u>3 \c"^p*j}7>_4\o.N+RӢL!]Z ??wN? ??wN? ??wN? ??wN? ??wN? ??wN? ??wN? ??wN? ??wN? ??wN? ??wO:>I2KIZc (Vw OQpċQ"hTDUTU@P@P@PZ Ɋh5Ҩga|/Wv!Y@<~!B xu c4[3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK3'tK a/$pg'dvEI#}?𽾁\\̡f۵Q3ɁO̱VUfQ@:( ( ( (I$]hIXIcfO}FP@9cA~ttttttttttttt>gNXۂ dIwXE7|8J+;}>;XP@G'ՙVwb]YZ@P@P@P@iIev"I%p$dfFXk2,d9ƪޡA ?;];];];];];];];];Q_oio$%$H-m KDHx˫;T'P](8bE(c4*" *P Ƚ\I@P@P@P@P@P@( ( ( ( ( ( ( 6Ie (*6`#0P\}s@ ӿiieP??ZwTr-;*9@?_KN/ PA(S8- LE"H}J~(Z( ( ( ($I 2I:{(tH7`@oGĦ\Ɵ y7gA`5|frwRPDJ3Mtk _5m6C`gh4ϰTҡrKTe?kiϛP?ZwTr-;*9@?_KN/ PA(ӿiieP??(t@d]@Ia AG Ѓ@ @P@P@P@ $I<RO`T: \_'?_KN/ PA(ӿiieP??ZwTr-;*9@?_KN/ PA(ӿiieP??ZwTr-;*9@?_KN/ PA(ںFWV2G#A@ ( ( ( (jJ0s 4-;*9@?_KN/ PA(ӿiieP??ZwTr-;*9@?_KN/ PA(ںFWV2G#A@ ( ( ( dGDuEi={TWKN/ PA(ӿiieP??ZwTr-;*9@?_H̓ ?L9_[ 2z{(h (<#"o]s@%P@P@P@P@P@P( ( ( ( ( ( ( 6Ie (_CLSM qdc̃w*H? ( ( mc #u) ~Mxpmƾ@eΡ.mA-\Š@P@PgKYv-ǜ,TuS[z*H;Z( ( ( (+wdV&ϓa.ȊǺmX qP@P@P@P@P@t;inCI4ײ`| @@P@P@P@[{h`}2. L`9nU IP@P@P@xbXaYÒ$h^HRdg$%zP@P@P@Pkj|y^j`:nvc$`rh ( (;? ^J=h/>1m{X?=ϟL[<5s5H#T)(U3s1(QP@P@P@P@`z?i$Ls`D#쁽?L=ϟL[??&-k~Gɋo?@#ŷc|b>1m{X?=ϟL[??&-k~xwW$3`}q1?@~fFJ3 %#Aϖ[l($ 7 ( ( (0uN4&T0] "|#ܣa$m@{lf??&-k~Gɋo?@#ŷc|b>1m{X?=ϟL[??&-k~Gɋo?@#ŷc|b>1m{X?=ϟL[??&-k~Gǿ-~tz9*g*R8Řpc 6b@P@P@P@ejbv=9mlacFH 0XojH<?Mң~j=Ҁ>1m{X?=ϟL[??&-k~Gɋo?@#ŷc|b>1m{X?=ϟL[??h#Oo`詜HCDa;(ګPO@P@P@P@^3Js,,rBY[n[$P#ŷc|b>1m{X?=ϟL[??&-k~Gɋo?@ <;>D#y%e{ْR~-˶P@{O. ( ( ( ( ( (( ( ( ( ( ( ( :F$:I&PI5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(5I@ٮ?J>q<&RPk7\ Ԕ}xM~o%fߩ(<&Օ ے2# 短tTP@P@P@P@P@P@( ( ( ( ( ( ( >LtP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@zF Oo3PP@P@P@P@P@P( ( ( ( ( ( ( >LtP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@]-;AHDqr;80K(wDMgfc@(誠{47>>PM(?ɿgC7L(&cr}3|Pϔ??o9@>>PM(?ɿgC7L(&L?~?/ 0VAD7Kfe>[sega9( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (=#@Mҙb ( ( ( ( ( (( ( ( ( ( ( ( >LtP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@u>7G@?ހ ( ( ( mpC l y5P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@&L@P@P@P@P@P@( ( ( ( ( ( ( >LtP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@u^vP@P@P@P7?q@4P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@zF Oo3PP@P@P@P@P@P( ( ( ( ( ( ( >LtP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@u^vP@%gUSRvg8NQH%9ֵ K_2C1Z|brn@P@P@Ǽ_h ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( A6Jf ( ( ( ( ( (( ( ( ( ( ( ( >LtP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@kax\2R9 NQrbFITY#utat`(?NOo_^F 7H=g;/.|ۉZ; s)@f/j+]#t|5F0lX:mlF/ [}ɦ~@P@P@zޥ +\FEq|}Y h ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( A6Jf ( ( ( ( ( (( ( ( ( ( ( ( >LtP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@Hge('/MNi5j5!ӋnB>ZhrI_o50狼AIѲKQ֚+ xkPF/,F_M$gK-RHҰñ9MM@n?JP&i )]ITe-;RQ|$4$'?ć+C $I $$xJq&`SIVG+# _si(M@n?J>q=Pn'LtP@P3xLv57wK#$l0ÐH@9wIkbÜ{}w7mW}stm϶~.M_% >?6OW?@&گiUm?\]j߫KmW}stm϶~.M_% >=w7ռQfk{8u=ZH$S]%pME9ܣP wK'j#u Ƹ +iW0^ M@&گiUm?\]j߫KmW}stm϶~.M_% >?6OW?@&گiUm?\]j߫KmW}stm϶~.M_% >?6OW?@&گiUm?\]j߫KmW}stm϶~.M_% >?6OW?@&گiUm?\]j߫KmW}stm϶~.M_% >?6OW?@&گiUm?\]j߫KmW}stm϶~.M_% >?6OW?@&گiUm?\]j߫KQi|0umV :]ounl];[i#,J )5|3oMcF|kxGڤ;VJLlLFѴ]+?&y6OW?@&گiUm?\]j߫K oㅏW fo$gUXY-]],K#;N eb|:4v}ȼh &quޣl"wHP`9ԝU52:p*VA@Um?\]j߫KmW}stm϶~.M_% >?6OW?@&گiUm?\]j߫KmW}stm϶~.M_% >?6OW?@&گiUm?\]j߫KmW}stm϶~.M_% >?6OW?@&گiUm?\]j߫KmW}stm϶~.M_% >?6OW?@&گiUm?\]j߫KmW}stm϶~.M_% >?6OW?@&گiUm?\]j߫KmW}stm϶~.M_% >?6OW?@&گi vV]dZZ[]]mZI#5#ڻͥɴźʬT^dwQ\Y:P'PD,[xjgcO+ L#f`eUnN66OW?@&گiUm?\]j߫KmW}stm϶~.M_% >?6OW?@&گiUm?\]j߫K ֞8}^&y՘2_*R@ͥ qk*aTp<zARh (4)6( ( ( ( ( (?( ( ( ( ( ( ( >LtP@MI5# V/((vp섃PI$I9$I=I=4P@P@P@|eM|Wt˄{,[H|K{nɆ #J1nEOxg!ker,^vS^J\[ܒ,l2{/T"fsGk,nǾR %YQ1&$Pj ( ( ( ( ( ( ( ( ( (|-&8xoÐʩ}ߢʼHmc$ok$qx'/'DŽ7-1]?WQ2YxR NO[\+V wӝSԞ}zv{ h ߊ>:߃u?i=yvZMRr$JYnUtfQʍw^K]Zƥ^d1q܆o08DP@VͿ#^w%΃mo;95GAnYQ0a$h!%J@P@P@P@P@P@P@P@P@P@P@P@P7/|G{]tkm8] Fkgh?wlmF4(PYk-!d[bm5Mᥱ[9d/x)%P? xѼKm569x$9KY2ZܤKr4@P@P@xkS}?Q2nLo@ѱ< zP@zF Oo3PP@P@P@P@P@P( ( ( ( ( ( ( >LtP@x7A7}=zPP@P%xk׆xQN4pw4}wn<{tt4PE4D<xA;5QX<fo,Q#cɻ4C*OгP@P@⹋ߌm{g,eҬԩ=T[hJ PW?>8C 1e)-> ,-7 ( ( ( ( ( ( ( ( ( ("+.H.-?UͽxuFP{-n>y-o|RLtP@ں|:58@@ROEY'@5$rC#*4rF]aq~YP@r6xkQgLt7=occ߻q[F7;c$>Y[_ěkօw/!> idOH RmkWo)@5}U@7lHҶ[Eq=MCkkk V$" Cp jj*(PM@P@|D|pxik]GL 6B^Hd$l˺"Nqmqg<ַpMkso#E=Oʇʫ$r!2:)kw:58tiwZEmd8 21˸~|#qO #կ%/b˚DD`,K8`S!YnLq=ƀ@P@P@P@P@P@P@P@P@P@>0x[υ&- Q5 wQT -+mܒjMq-6?⟉*Lj|Rʹ'.bt4s6ws[Kp~No9X]Fd> (9;O煵_ j,bmc} l#RWqeS$a΄p4˥xN؇qi|Q.b:*Vܷs2EY**Y$< MVSeͬn|;^FciF[y"l`Vy%!{ ( ( ( ( ( ( ( ( ( ( ( <5|-m͜)nG2 F5"_y.%mrjix&.E웝%(V69LL6{/o~%oZmG2S_P)"C,as#;+aNaM?J>"P4Y2M!Vy%jP@P@P@WH/eB--H%Ncs#bI8UN7P@Ph S5lP@P@P@P@P@P@( ( ( ( ( ( ( >LtP@P>iqIs 6 J )^ws/T$W<qE`|s/E?KaM[ƞ4L:F<[渙KF\Ciii%|w?l;2^_f{0/eWYTcuXc٬K"+P;_vVvWZYA[[@AW+0qEFF? /_u/E?K%t QO?I]S$A)_@z 뎲kvz" ,5UZ`@t^tvoeyV}g S$A)_@ /_u/E?K%t QO?I]S$A)_@ /_u/E?K%t QO?I]S$A)_@ /_u/E?K%t QO?I]S$A)_@ /_u/E?K%t QO?I]S$A)_@ /_u/E?K%t QOύ4oxŚ%t QO?I]S$A)_@ /_u/E?KË=B/meKAry[Ӿ xOH^xwKl2z:ȓ'=(%t QO?I]S$A)_@ /_u/E?K%t QO?I]S$A)_@ /_u/E?K%t QO?I]S$A)_@ /_u/E?K%t QO?I]S$A)_@ /_u/E?K%t QO?I]S$A)_@ /_u/E?K%t QO?I]S$A)_@ /_u/E?K+g .jKxY[x?moF77.eGH{;vv-@W=^*ֵ_V}xn,sc$֍pZjl4V]<ŵ&m<K 77.ׅ4YW3$L:8~Cop[1(HOHD4;*`3_u/E?K%t QO?I]S$A)_@ /_u/E?K%t QOi\={N|/!ȞEpFCE jzNI$@P@Ph S5lP@P@P@P@P@P@( ( ( ( ( ( ( >LtP@P@q=~SVX4}F͹i[己/e!f 6g@Ox?߷7m~'N<3=x^oA< f |[4xc1zD@vj:giiֶ6YYYk *Ao(C H*"(P@P@P@P@P@P@P@P@P@P@P@P@yƟ> ໿x'Kyj#ռtMv{Smam|_e@xwZ7s?V,hu:~ty[>~Ӯ\X=Z݀~P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@BО ְWT.[xGPL]UToHy"}KQ(%xpg_Ş)ah}a+cm@P@P@P@P@P@zF Oo3PP@P@P@P@P@P( ( ( ( ( ( ( >LtP@P@uoռe-ZF4ks=/# ;82_K ;8繝8Вh^_?u ~ξf#&i 42\I_4JPŭQ6S WTB46OӴh,ll5X$X%TQ(€[ Hd+x"y(}^G!TgO^9NO>6 Lv17g͑GH-3Vh.e,w\6-m[Hb!>)-s_i*f'YvP~Y\*ҁum Kqkji ++(n. K!I"rFxkQ iLi"U=FF(?X SOA/3Sh ;T?b7?N?X SOA/3Sh ;T?b7?N?X SOA/3Sh ;T?b7?N?k* Y*ďSee.l.[yRT:(NnF/B(P@P@P@P@P@P@P@P@P@aj~&tg-R`0\(# ,K&q@|Aj@Akw7:kZK/m滨fH4f95-AAm W7ZZNkF?iMXj<;x&MԟK^e̖e Es&%:[;k>$w@?Ab7?N?X SOA/3Sh ;T?b7?N?X SOA/3Sh ;T?b7?N?|Dq l}l50?l@NմZ#6}oy f%HBȪH@P@P@Ph S5lP@P@P@P@P@P@( ( ( ( ( ( ( >LtP@P'_υoxVѼ?5cas-m.$~ox[6;m?hɠ |-HZibgjsyK}mҥQxz>P1 8JZ(sk{WeaXO$p+~DnLly  ( ( (: ÚW跶wuo~YdFc(E4i,l:4r(taTP@P@P@P@P@P@P@P@P@?G/]֍Ckj=DHXUd`GIFgGbĖwv31$1$h˾+|Uo ˮkr n<4MouL ;+H@\mm8%U/x>6|gCs|!K,?be]8L̰Hͪj>n>p ( (4tVEPhgsAxfAĐ;2p[:ZcB yo ?yZ)F eN1Р ( ( A6Jf ( ( ( ( ( (( ( ( ( ( ( ( >LtP@ƾw[>0խ_P[BfK{+;tH7P߆9oۿ6<ݶOٷZH~5HXy( !Nkx?Pt;O-+Jt6 ?O;k;+;X{[[xU""E(EDE @h ( ps@  ( ( (;t­|w6F}8(&Xj g56ܹv4RG̞I{{Z(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(^J$rU[x[Vʴ _Ptv4v?^\'X`eXf x3?>:u:sy >""U (g&hy.-)H~i(->cd( ps@  ( ( ( áwL*xN_&W&mI+cFZ59 8,(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(^J?)׿%Juk/D{Z(ͧ.aA?ڋH}`6+x\ܭ_zz_!xyvzn#O)|+>(PŸ MoGY ˂=+H[qL|#/of<acX#.粔*8w )CK$u]Dw*& ( ( (>xx;F#?mJWؠҀ ( ( (4)6( ( ( ( ( (?( ( ( ( ( ( ( >LtP@s2񗆾gVkm%]_6TAKn.dInneYh_xT RoE|,eqq$2aWՠHt^ox~g}_WL4N4]3K-`#aR(`$X4UU(P@wZ6y+)sc<|0] i <2cPP4 ( ( (UgeTR*YP2I$2O9o $Kjw0 Kon^IgRsEXVHVa(@P@P@P@P@P@P@_)x?u/QZ]vQ5)G-ZHޣxco.=)5V|ᇌk|~c&M_ZV:2fI{o1A[+ӤD8c5T4Pj*UU@UU`` uP@|]z֭eͥ^J24J\!2LqHB X8( ( ( (>%,חQ/mloLtP@׌(8a!H$X($q*"P (yzȓ dD%,iR;%Ğ?\Ps' l?s' l?s' l?s' l?s' l?s' l?s' l?s' l?s' l?s' l?s' l?s' l?9|9`ɠã.b=??U@ =(P@P@P@P@P@P@P@P@P@O/~x.^1n{wCVh2#t̹}6%ˈ g?~3mkI~vK nΕeLtP9xsռ[ZDjvc5¤qVK{;KtXyQGGm{xW?Y9-y+%ċORYmi%д9&d@4/AEҴ* 7M;k;+;h(-(*";@4P@P@WkHqWBN2<3A$v!Bg)K7A=+SKY%_@%:䵗"P N?-eȔSKY%_@%:䵗"P N?-eȔSKY%_@%:䵗"P N?-eȔxRhfurBv"56$o)kzEP@P@P@P@P@P@H4Xh+Z|6y$k|Csa;j/& #?$i 2LtŞ7uoxVQgl((h ( ( ( ( ( ( |%ƺZVCYsyrᅮnWԯUdm#ESM Ok6~.?|;y8u5'6ƻ)&xWsXS?]% kk{;{{;;xm--a$X%Xc(R4UDP ( ( jK./F c8:J1h]6dY VGYXpC)=hP@P@P@g<;x^k)u)$ɍ0"?u  ( ( ( ( ( ( ( (;+Ww>6-41òMK[Gk]#Hg]b2 5Yqq ~x Ŭd'¯yOy[mKQ#F+GO S#x3maa~nU@UU*P0`c<3Ek{ %RXRU8ݸ%_h!@P@P@P@.fVOK;;Hca,PAHC J* (P@P@Vk+qik;zkxelt+timҿ'JfVn+[?timҿ'JfVn+[?timҿ'JfVn+[-:Ccg Ckn>>(P@P@P@P@P@P@P@P@y?O6ԖIˆɨjH-KךF(QDoemqq ~şǿ ]|/6:E"E*foe9Yuj#6v7@QƑ"E$qƋqơ4@@UEPU@ 1@)I]a2*rϊ7O+o7@N@?:W4td_ 3O+o7@N@?:W4td_ 3O+o7@N@?:W4td_ Lc`أ@GP( ( ( JV`hZ ( ( ( ( ( (( ( ( (ǟ_[?_1D%~|$Ӵ_k~I㸱>3|Aò,SåKYMt#augqp8#??о(}V~.ƙkIHkº֧hBמlڦei p[[[k/, h-nn#Ym!gx'Xf9cutfV' (mC ONvMOrK2x-?GȗTIk77N ʼHb?Jh ({Ş,灼9xڽ[[U˶i&i %f(ǟP/˯Me?Q^(2ڧWdg$2дwWq4~)n5oDд iw2GѴ{Ht7Km,-$PC UFI9wbdvggfԠ ( ( ( ( ( ( ( ( ( ( ( (K>9 j2oh-坏2LVyn" >|;?,g>²i#]f}|E6g)5]IOD6 ;w?" !QcPhGhUP ( ( ( ( ( ( ( (cNѮdRq"VP]@^PƉ$q"T}> ( ( ( ( ( (( ( ( >:wO_%X/|oVPZ&>YUYY+2UG?koIiV|a o@S}_|F}xZvZL=wVw6+K{}GL.m?Tট ?{_'>ο:3xoo@mon|-RhZh7K-fd[o*piڧO%GY?־ A>2;?_xDxW_`lSHc=\znhR5yUjo*O;yxWI7% /4º#MϦYcZŪ߸AHxlHg'#Z߆Z_Gm>y {u/^ ,$=奘]r^=[+iﬞ+T?'~84-7/xi]JAmb>&gx~ t_oھW^ҵ)bKMY?S>5 k^ o;O >#lu[X xVUMJ;Nmʖ7`D~Uoۛؗ MxzǾ!<xBƁxs^3>_b6 wYi 66ZDqO-|]5/?5WiSgqx9o%\\okuLj5IfD4(5-@lPre~^ZwWӾZy<1z=Մ^-<1/K4xv\ %5آn`tnR𗉴W>2ey?F64$Vjt"<%.HB: ( jZt)u [V%m-ap,9XmInnd)&&s _&#z}sj5k6*줤ѱIC5'k+3>wux-J׆-@,Ӵ'N-l$PApgv-$3+4P@P@P@P@P@P@P@P@P@P@P@P@x^kMg5-t/ h6w7LpġIn.'DF <{^-4;呱%ӆVkd/M\nV)"q/?,/<=?xjqጒʑb+_mՌok[Q|*VKKXb (-V(a4X5TQ(@& ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (  >uVۦx^$10[ך5 ,$q$mGW׋-njMSM׌|{|Jm}J9u;Ng|)zrjς~"F'kmG4{NN}UGl%-do;4ڇ~xzޏqo։x/t|=uxo|IgK_ _,w:m}iwm:P6B> ~W|M Ѿ!x{m1 %6o}F/oIŦ\<{|`z\~*|NFæ(R%-|G_^ZJK] >Mo,CMO()/J|I? ?-;I {ÍA5[YT ijͪOtMkOi?cӴ"I/xºGk[Hп%5MFTNo =Iu5rw?_A೿쿩74_jA:Yx'g(x<0ux3úIɹO j7tc{?_xӀ0 WFnGUt=U0\ψ!E2/ĿHToT#Hx+qPp4';V??MgjoUׇ( / jZt)u [V%YZB>gv݊EK%đ[[E5FѧࣟD k?E~!g$nIur:}/뗒(7CAҼ1*Ce:t+ 8zrgyin ( ( ( ( ( ( ( ( ( ( ( (9x/zŦu=NȎ(P]]YY[G-ܰ$ƌYǟQOPxve+c#5+Ghe 1$Ү5}V{m I|9%ֵkt/ADI3O%Hc4UTUzP@P@P@P@P@P@P@P@P@P@P@P@P@P@q~"COxXDnfVm>{}_M\LʣjtwBvP=Bq$/ -Dchx[_z7֠vvvuXgeegv֖v$6ֶЪCooo $P$qF@h ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (oS"k}*/+࿆>#]Pmk^^i;;aeZgkjnҍ< Ś_< C┒j>? 6zދ ˩cSԬmuOq\E8mK],L񆥬~*:leӬ5kzap!{g>m kFb@ [#MԿj~yߴwu|HTxҼm_Cxv?|.bЙTwp 0l¾<φ-?15-R+?^(ko ^N=l Q-x¾W3 uXQ.ɧ[/ͺwy ?Avx mGVյ V.^Y\' j\LAr,h ;k߲?zMZͽ>䚌?^77ڵ%aφtO-h^Ь'OCieiG31ifVygyn.$ydvܠ ( ( ( ( ( ( ( ( ( ( ( (o/׀|5ŞKWUdGUUVK{;;x滽KHf8IҼy|I3̿:/zRM<>T/C$־]D]fVTEtiniz>[^kcack%X(UUTt9 :( ( ( ( ( ( ( ( ( ( ( ( ( ( } 0V$s KH3^2[X۬7w2 1s  =OŸu9ǀIms㋻iZn$Jhn)mLfn٧:UnkI*[‰0ĉQ* tP@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( (8Y}_:'P # §C?d|;T@,>/ Su>*t?NGÿ|NwC:Y}_:'P SM? ӴKۖ6 &i<$VXъ rM+b8y?1&Qψ-kjׁdC{-S&f`R]Fh4Q:e->u~x_W xSHм;E:Nkgk8D\$5-Ē5IJP@P@P@P@P@P@P@P@P@P@P@P@^?uW|9KjڵUX8WYvH-`[b[?(y#zgiTY#I#F^XwuOgM>y4:Ift.Z.AiZV[YXY[ {xcQTz1.@P@P@P@P@P@P@P@P@P@P@P@P@P@P@ٟ|i-'ƟX]?ٟ|i-'ƟX]?ٟ|i-'ƟX]?ٟ|i-'ƟX]?ٟ|iLo#%4<>08}>u+_ú?%Yh>l'Oʶ8%idgwYni%` ( ( ( ( ( ( ( ( ( ( ( (0MmZ+)[V%YZ@2HfbV8`^{+{xX`_G>!dׂ?d_>"jg>I5|;=tۃmq^^д gJ4  ;ItV ;;t0ƾi$r+uX|7Y}RxTEP\JY5ܰ-&(8|Y5~%sÍ,Z-Rg"KWt:o$_3˫ϫգW;Bд=#F,/K㴱a,p Hh}hJ ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( oC𞇪ĺS:Lv6V^Y畸CI,D#0x 9ZkGMTQYJI'KN[[?xWºM?It6vygش\]\I-̳\K$@P@P@P@P@P@P@P@P@P@P@P@P@P@P@WxsXou{=Ú [Z[Ey%Y 9nne)n&7ok:Zςdk/{-O&fZ7W}4l]e<^-Ƶyj,ѡhZ74m3,mFӴ+O-,mcAmmaV8Eq9f,P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (+]to hڟ|Cheq꺮:[YXYZey qƊI$C9U`}BQrhz,ςdQkjSY"%]^,6r֞-_UmnNXs~|9G:E; 'J˷'i摞{[˩fk:( ( ( ( ( ( ( ( ( ( ( ( ( ( /t|SYh^Ьum[PCieiff%cI'xyR6DsQ'G|{-S&fpI#Q'!L~n qP?wBxg:UhvPiNBv6v(a}#iZII]݀6 ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( u'N뺍iwjXD\]\03P䮵x / o xV}cw#K-Kf4p:mU6-]}^}#H@ox;?3x;=;/KB™fwv--į%̓]5$P@P@P@P@P@P@P@P@P@P@P@P@P@P@P?)(9> \vɪxkh,"e!ӯ J!,oIBR+x@$U ̡(='?=GPоx? -կx{Q&g{sgg&sgM ]Z[5ijyPW*~/??;rզ? 'fi7_.ſmM75Mr=}e/ҢZ՘[=zmڇZϖv{P@P@Pu]3B/fJt;CRԵ ,l,m"infd x"FYduDE,hS~"Kǁ| s;TF%-XD-_[.x[υo/G|7Z%١ jKI41ingk۹fYw` ( ( ( ( ( ( ( ( ( (;O~iW⾒  4*/wg xfB/j:7.>#Yh 5Nm4sX^߀5xCE; +JM%K=3]PR*9_Ojuk㯇:g5x?Wxֵ; RX/=khks˞Wv฿<׆~x/?4o4Mj?x^c>or Qti'g}?RTxoEU?5֗I/|1iZٵ\hR鶗sj֧Iǧz.|YP4oO_:%ͮ|9_MWþ ad𵾣g{}sQo q}r4H:s IQ|{VJM--5$φmVYF)oCs270-nTP?l߇?mrME~4+6-<)/|0FNZuXs߼^]ee8ྟ>kVz.>/Ӿx+A _>oF|Bմ-3R+ w^O]Yk)n?U?d_*K~?i>8|k 3\ό^9G~2^'%|-y~ןP&g/>'k˿hx^_kλ/ xi7֓uiww}omVwV~^U7.~Ikk׾'MO 6cs-ljVO[I׶@џ7 l~ѿa&V6t|WFk3]6HtkWE+r]zDҤ٫Yi2g߲ u3^q [𶙢h]et xzsx{úφ!5))h[] R%~˟ 4;⇂n4 |2=g ɬk<^'utϊ&M"S:v`Vy?iMy.="|R:!hlO?˥Zљ5͉% ( Z}_Zi^iq7ZYXZDNomo o,Jq;U~Jx~;|BmTVN9so[?z{iZM௎MLj즵̗t~ݗz~ 'q]_qg^hf|Wm#΅c4>O]n_WKui;Oɧ5S{K?3@^kmepw fp|=g|S;ݧ 뺯o-[Hb5(-2u&멿2"~: ,_?:'tt_RX< 6L?Y}w>fwL>h1x㎫ uG__x'|9^hO|Ze%f`l}K΍X7؃g{9&fM Kozx(%55X[biHO*O懩xuioB?<9siY6RItځ- o/-_؋Hmo׀5l5;ﱮiGе|E۝6k[bmJ𾋥G' ;^xMGK+>W,E.x=kww LKtmDjcc)`Ƞ յm/A o[,#I5=OP&fHYUI=`Ok;|B^ (WOg|@f9d*'m$~"]RmFO~ nio@K-3LBCK#inni.oon^[۹[ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( (;O7>P1TU,U%+ߴG+)e`H 4S""7VeV1ҟ2 G>d'bYpv;/f sW Kai{sxǞ c̲SVΫBu<( ˫Bd/@?ିo~6)PZ_{w<KO2Aw ^QDfӢc (QcLht/j, Jߪ|I񅥀|{_^əIg]\untim=ը-]>4~]x⯄o_~Gsƥ eݎaK:;6^!|畯e͖ K Wws>|-s)կtC/<?=&x)u+O W7֚+Զ+i7vN}'sIpDŽ~^!-O>\= {^M>7Aot<؇P-)NREqWFq|OG Z?=:^}5}s_h'=Vz{]hu$k@V*xɿe?i/|9oj8{ohj=ST-t}bM'U5};TAI%p6yiqZ$}GNik 4ϋo?uSWN[>Kmrd^KDZv:_XWipWE/?/şl5/Bm(~"w^-KV-Υkzvn4I5J^+[k)sx{@Kռ#s^^1·VzNlu-^#qckKnF~͟_!/[#^_2x/tt~/=S~e@nHMӮw2e7 k?Ex'?׈~{~.x|}c\ýSW)ѶcizR+s#|gهnSË/ |-moZ<k)Ο_K 56zZZ\rݾ궋@>+׿Lx_VvW77◇#qw<h4_Z]/HH>AZP: 4$ WokJ>|K_Wu-}CMN~[ݜmnh?&P9gG'_\^ _i7!?/t/(@h( wv}սwsGmiiimMquuq3$0[,JQI#*)4+xygg? ~%|HHd̎dk;_kƾ"R;[{+;pӟ<7< CwDEoo=]j:e.gJqw4P@P@P@P@P@P@P@P@P@P@P,5GǏ~?+oNtmG^ig}Io[x]bX-Pq߳ UxW<{2⟈Z΍y-MkZ<9[χ'T3"^bTXث~ U ˫?[狼E蟴W}mui=މ#I5A}4ύmI(]|-qeEu?˭x/?iЏ79tvŀ?D5W^N[nOVGWd}CL f{SyEUyŗŋ6’Ƚm/_c𜗂 [㏱pz A&@/| Y|6<曠 XxNдOkbt|= )p$;$L?3"''^/7U慠A]?7Gƶ:Vi 4848Otք,TdGs+^Ֆ ϫ B~$|aa? `Mpvk&vM#K& eC2S~zмo ėGl" ?0ؿⱰ6C_K'O_᎗ogo^:H>osBVp ~kۆ{1ky$ydl@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ( (Su>*t?NGÿ|NwC:Y}_:'P # §C?d|;T@,>/ Su>*t?Nį("gohJ2o $N0hGO_o~| ^@~(O)R/Sn[ MՄ ah:֫q@? x3xHHm@ku[ڭ]2d06P@P@P@P@P@P@P@P@P@P@P@P@s|)ܥorB1Ν\l,Ip]+B4k*Fx@őewC:Y}_:'P # §C?d|;T@,>/ Su>*t?NGÿ|NwC:F$<q;BTDQٯªY /y[go _~&|IHeƆm+i3BHLj5ӏ 7< CxwDʷ<=jZKN\{y6^i $H ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (-'ƟX]?ٟ|i-'ƟX]?ٟ|i-'ƟX]?ٟ|i-'ƟX]?ٟ|i-'ƟX]?ٟ|i-'ƟX]|v_$:,H5M֧^qjWlgv`+ P@P@P@P@P@P@P@P@P@P@P@P@|?3@O[zg<3k&O .RյXz4呢LXٟ|i-'ƟX]?ٟ|i-'ƟX]?ٟ|i-'ƟX]?ٟ|i-'ƟX]?ٟ|i-'ƟX]/|!v_h~ҕpFZ[˹n=NBxXGm 1wP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (icingadb-web-1.4.0/doc/res/ListAnatomyOverdue.jpg000066400000000000000000005405341516124654200217060ustar00rootroot00000000000000JFIFHH@ExifMM*i'8Photoshop 3.08BIM8BIM%ُ B~' }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzCC ?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( i<M[qJ_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(_nӯL?&?Eۿuɏ@:hwcPN1(b6oݍs?tc8 P@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j (?Qմ&jwֶ0󆹙".G%bF!~$A<-h,u6nz0b q: 7#z=v(϶{|6inG4Kum- E1;#XX$UyYӮ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j@9$@mz+BS{BK?,iA€ N (u 4q,ͨiT6y#"֓Z0uP4rrNxoWlצwǵn%ZHf@H*>\^)@!X:HP@k>ׇ%kkƚp^?G@!!sJ!!oO_<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-_J-Rt77:liDz){xޚH (сAЏQׯ ("hm{cQi]c8eIEP2Y V<%s***uݔMH:vv]\Cuk:x$Y"u>WRFAXX`a@,P@P]Ðڽvf|@ingʷ4Bȹ] |f]p[[oo@ oNt? xuCu, .[yd=Iyހ ( gg4Y."bGB&\;pAVHvFdT9 /] t[PeV!RyKK2D: n`C'_^{ f#!'EIr9D]erskP@P@P@P@P@P@P@P@P@P@P@P@P@jnh׺6Vvr Y "BM‘9D֮Tڪ6;qo E .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];$_|)#{]n$$܈u wڄf=Iִv^W^[2Cϗ|ǒY3Vwap|N1۲2d6hP@P@kzޡFKM0 @ AFAr?ho^" #%1]*.P?buU;&m ZO&7Xh.+H>w%!oӸۻ9r>όVq(>ߝ6o7Poߞw}?;Mm&< ( PҿB#v"vu:s#ns|{@z?Ÿ;]?ݳڇ1y?cnqG^7mK+hj(>) mcq,Th#ӷn?b>}cy;>^{PP@T|!O7v+g8uoxcosiuopKKzO~zŤ4.ܣCqC aKpcb fs*~X1xv;WԴ][G=>U80GbOHt= ߂5\F[Y~ukyeqccT#Kt=*JYBǻ䴒ɀiY吀؀yŏkZqZ,NРep.j9g rm|@O÷>&֭4csod 3t A+gE }0z>P@P@P@P@P@P@P@P@P@P@P@P@P@÷l,oOxs#jau?>ՎRJ8( ( ( (:vĚqhum"pάݤB,]cdn]iPP@P@P@P@P@P( ( ( ( ( ( ( w?7jsp^/ rWS0Q,P)@P@P@}O-5;uVX.?WLz(7m `=팟f0;&  (+ _Vʣ:?K[ ӿS@) Zw*h/o+NM-?i_;4—RT _Vʚ?K[ ӿS@) Zw*h/o+NM-?i_;4—RT _Vʚ?K[ ӿS@) Zw*h/o+NM-?i_;4—RTZ\pmu)#!ӝ'Xtco KȀ!hрAӧ ("n" QdHaHu`pU"<o%ib]GNI1X"yoxPz*68U@ ?-?i V%m*+jʧ>MK{1IzvZY[kk#XE~TPI%bX,P@Pp:w>^|h`'ʸZ2 ] |1"]@O[o$o@ /o+NMt x~u3 >,$(+xH|9ހ ( gៅ5f%bILVl=rIfxFv;g9 /RT8f&PyK0L:t2 nPC='@&e$fgFyp8GmpjkP@P@P@P@P@P@P@P@P@P@P@P@P@i:nhZ7wyrpE"Xe%@H$PMo’t֭DPZ.{qaq! E _Vʚ?K[ ӿS@) Zw*h/o+NM-?i_;4—RT _Vʚ?K[ ӿS@) Zw*h/o+NM-?i_;4$_){nuvؘt$b=It YiVqYۃ0KǙZ ( (xO_ZZPO[hۜI2@f@?-P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@iPP@P@P@P@P@P( ( ( ( ( ( ( w?7jeVR+VR0A 2#ps@&|BdԚ6mF{I%m%lJS1)h ($n%#yD(RI#TDE3dºO@Vյ00FÂo1IFX ͼt}9zH!//m\$}@(%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ɠNosmyiqf9lvHِ O@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j)jug>[ͥluVR>dD!u|78X,q2&4$_Ʀ&2, ʌP@ZtZeח2t 9EWH>/ë_ u7[ e;eeFۘUFP@W ڐ2ag!gY-$`jˑ@ 4=IJO(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO зNommgio a8"\v8P~O@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( (bpII'jMODŽ뇶I/6m2[\ڬ=$ ;G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .S:;=Las(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)b= kJR׭OmnF۫bmFmx`ˎ3>; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( %ׯ9.,]R% byg$+68Y#b[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@[>3V~os@-+@7όߛ\ g ?.h?h4{˚?lZ|g@>{^5MB+x/.Ej/_[&q(Q3>\F@;J( ( ( ( ( (( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@7¯QB ( ( ( ( ( ( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@7¯QB ( ( ( ( ( ( ( ( ( ( ( ( ]"^ֿs@ P@/h>&?x4٤o xmN4f /KԀ $H&V$Ek*z( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( WZ?P@P@P@P@P@P@( ( ( ( ( ( ( ]"^ֿs@ P@y }P>ZڮH0[WddFETk{y@?.s|C$m` G@ek֛#Qu{tIo/'`Na\2K!pĀ:EguK) yw&i!ǟMŊTԯo&VmK#Mo:;ynahkOKg nu[m^H8Ik-(;E<Hm}[h*mu6x' ѸY!o{y9Hղ(j>Kk xgVz|YxE.cKoe*I|Y`#|'/rvPrD~'aO85* G ;OYxz7?h߈꺭Nj4s"O7m~w՜5sukjz?I|3=+=s>-Ż0 $gsG5 -g[{0IuVp7OSk SUZE xS!UFYݖ4U>P{{-;O9O]G>hQ10M63@W5.+ݺi47mqBp孚JC,:DEi3mdAh+kH[[}Iy+s\d~n٦.LioHUԻqI5- B( ;O[uM_@2@ĚDCsu,.=UЩA>E!lǂ[/#;Suvi!m'vvo.XY2 ( ( ( ( ( ( ( ( (? k_ >*5xPV>,+2'X"%{ (mo]EZGzu' \}xej+aQ+*ЍIN 1^ϑMS7CL_q.K˜2L%Zr|ey2'J8&2xV 9b烆"8O7€>U"qO<4TP@P@P@P@P@P@( ( ( ( ( ( ( ]"^ֿs@ P@ֱIuVmt.{! EpĹyedwQ@{|FM"m?yh.'oG٤P}ܳL4 ( >*1j%fsضLIh<i~4/c={x]=WZMS$꺚ӭOj٠ ( ( ( ( ( (>/OxKelo?oټBe80sh_{⏉Ք?o}L|Zh C=֠&( o 2d[xEiƣWOԴ˨o,#$  H36RhYYՀ?e<9<=k'隲GkCx瓔m994O^|\խvh/CRx t7U.r}|@5 ;߃$~Ө˧̣WV#G$4u(4EK3|Iky,]ext O^A+! )ynfY'yifi\,伒$K;f$h*Q}";~&-:n׉u4}Pm21[d g/4xTvNoSHS IUP@<:i{Y`'FxmCx$YbfDVS~K[m7OlK&? ؠ P@P@P@P@P@P@P@P@P@1?x\U FG9fU}E Տ^tZ+Z9pQ82<8+v8< B>iW'))9KC낀 ( ( ( ( ( ( ( {z>蚦Lkwym"78de8օiѯMRZutӊgN 30 +2m®2Viu֜d7+l@ȅSO:zP@P@P@P@P@P( ( ( ( ( ( ( ]"^ֿs@ P@~4H g1I5:ΰc8j 5.  ( (:O'Pgy,P/ ?)ݮ 桦F!VK Ԭ!f<"ygL-&? ki涹[{ydx& s$ XG'sf*DsjڥYlWisϱCHnPݞ<| =.KGž!FZK"QG/$@TP@~v__ʌ&t,1%dHy {˛{;Xk⶷^Yb4IUGcg'q1D]P( ( ( ( ( ( ( ( (?zg^*s2g@Xeʺd%XjҞQ5:8xU_mtUs7g k`~ RfUJ*,+JҔ\c!@P@P@P@P@P@P@P@jhzE߈5AiSO,AԮ⳶2*N8kBЦ^:0Kw:P^m#s|'}TI6ݡK +c15&FhR}Q6ˠP B)F= ( ( ( ( ( (( ( ( ( ( ( ( ]"^ֿs@ P@^ſ>âڌbYYB) _ݎ( ( (Ct֟ZI3Jv!Df[4|ir0hoGBMm%pJ8G,gC*̉,e]fxZwq fe∟-qsV;i x<1D±DIk@+?Oq4±DIk@+?Oq4±DIk@+?Oq4±DIk@+?Oq4±DIk@+?Oq4±DIk@+?Oq4±DIk@+?Oq4±DIk@+?Oq4±DIk@+?Oq4±DIk@+?Oq4±DIk@+?Oq4±DIk@W~|R5VmGDgQ>8%ѭoO.LPφ~kFu9G.ȈNc`#H-mC kIfyШ񇂼7"Mtwld/ݕRuctuY$BOм0wΐET\\L=v<[G ?5xshD)NP{.<;}OǮCOiZN(ER@٫ 5 U:i& 6ms\F`p%Ds"JE|3d:v[k7}k[}HXx =[9T~n}k١$SjKqIVqG59+  O% M¯Q?񒳐z@Msk(=Y(H>:jleĂ+/;kv?j%8o.YX|2 ( ( ( ( ( ( ( ( (? ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (>U"qO<4TP@P@P@P@P@P@( ( ( ( ( ( ( ]"^ֿs@ P@׾x5_x3{V$wm-{wk5C403ƀ ( (.iza \^7v6p'-5/P췅U"qO<4TP@P@P@P@P@P@( ( ( ( ( ( ( ]"^ֿs@ P@|,!qA-:Fez,wЂ*0=ޝwsam=[Z\[D`UA+P@P@wޱ?UII<%cpW6iM˫,I68{.ciXmvv"QYFifY]wvѠ2uoLޏkYi]^\>HHӀ-$R!4M$qFx J<)MGeO}x$Zu8 -،\n` kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(@O^撀kω?? ui(Ğv8Hq~t_ iKj>owivsHNrA"ȥ5ōԸŪ1]?DNm WZ?P@P@P@P@P@P@( ( ( ( ( ( ( ]"^ֿs@ P@ xGlF6|C~],"OQm;dڒW1(mwn"8 2M,<]c@[Cv*2XP PA 8 AA4PXwEԵhI|$E1[E,J9g@^7.H5O7K *REΗqiOSB<ҼMFVVkMtLҬ>%GaUFrqfglh>j~ ~}m#Sgigc?ˠJ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ~ev?W߷}/yxGŊ>U"qO<4TP@P@P@P@P@P@( ( ( ( ( ( ( ]"^ֿs@ P@P@?xtVvnI/9(].z pr&mu9"C5xl1ڀ54 m]Ft5Y+@=*O-l--lmc;k8"8 (GZ ( wD{_ٮO[݋ye2&\@/pq}usjVy>>U"qO<4TP@P@P@P@P@P@( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@7¯QB ( ( ( ( ( ( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@7¯QB ( ( ( ( ( ( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@7¯QB ( ( ( ( ( ( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@7¯QB ( ( ( ( ( ( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@poe:/b7&#>ƾnʳϖCH+hQU;?([ټaiHt;+hvE$ $̓9$,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnEV@,?P|qnfC}?ր=i6PX&~Dsr{f3:-^47~P@c|* Au*( ( ( ( ( (( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@|O]>9&mX׮KX[Y]HrRuy1Gʄ5P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@t>5E^ꚍ^Z0H{6is<C?gȅSO:zP@P@P@P@P@P( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@5]YUԫ ! 0G<!kIm?}moddsh/J;e'8ȷ@qH ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8 7h? ~8DkAch\qTGl\3Ϭj3}TaYE .ʭ$VV3*P@c|* Au*( ( ( ( ( (( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@7¯QB ( ( ( ( ( ( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@7¯QB ( ( ( ( ( ( ( ( ( ( ( ( ]"^ֿs@ P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@7¯QB ( ( ( ( ( ( ( ( ( ( ( ( |^@ lNM$xJO6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c6iM@m?:>o=xt}{c2tV S 08ֵpGczP@P@P@P@P@P( ( ( ( ( ( ( խ`ҵ;+k>-ΞdE4{e7컣euUTXxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX v:6c}>Lll..$ĒۥGݱjBo@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j (?Qմ&jwֶ0󆹙".G%bF!~$A<-h,u6nz0b q: 7#z=v(϶{|6inG4Kum- E1;#XX$UyYӮ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j@9$@mz+BS{BK?,iA€ N (u 4q,ͨiT6y#"֓Z0uP4rrNxoWlצwǵn%ZHf@H*>\^)@!X:HP@k>ׇ%kkƚp^?G@!!sJ!!oO_<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-{FMjtk X$1jq-E$;Vv;c (+[w{q Yc8$P>fbAcKu[S:%vb\tx[|5ӿm@>-FUW4(Y'3\^y`QG.@E,SIʋ$R$rFrnсYIr: (9|p;8.yy)Iف0~3X>ï6;]?gTS-z]?U'`O=(|;V0鷸 \!Y6,.m ʌn (42"DEffl*K18Pj_<#G5J$;]\ki#no. φw-{^EI",Ԟf_vRdho;Xto-fx:68e$rxF]UU@P@P@P@P@P@P@P@P@P@P@P@P@P^izo"j.4@'ˉH (K‘DPHEihP@m(?] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<-> "I#p) %(CO!'4N붢Jܝђ'|p .TFA PaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7j>|ǒY3Vwap|N1۲2d6hP@P@kzޡFKMO<_!rrM.$'q畦-Y y]#P/t 2\!{%w!$hY#q:0tſ\_뭡#.{a3 Lrݱf\yP@{\ H[<^$h>}s} >rK]LoX-+(iY"wPJh^%ďqw5c鶲vGd TɁpĶv,I(Mֵmu59~V9İhfBz4NPL!,#>#H $m.Tq4txU]EfPICOv€>)Ԯ/f{ZYӲ*"QU&xnx.-Ia&+$r! 0AsȠ<'{L՜*( 6ȋG=h㹼7ZV5{eYfDuF5̭ 4F7zvm[LFvb1 0(ozLJQ.-.$i2@2I#na eWe  +aE heC)P4񧈮.P?buU;&m ZO&7Xh.+H>v~6=y Qݑs95;ȇ_7_39;Ǟ=( mC?OKP_>Vv_hq}ߴyxݷ((ؾ yj[wyyvms~;nzω_$kNVT P@X|'!vY}}~9w&ǝsOuݝvqo͚(|?v.ϓI~+om şKbgy@oSF GӅ}@P@P@P@P@P@P@P@P@P@P@P@P@PP@P@P@P@PPaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7j~1um-[0Vh]3H[Lg8h ( (A 8 cڡּ;jlۥaC.1Kn4@g |G+Q8Ŷ2@<41!D)b@:cF 2Ws,txJ]sE`Hcsύ0u>)n,oa{{YZ)aӳ+ 2:]  U&xmmy.$H%-$9 2I$ɠ<'x{L\,.w"K+ƍO(IIZu{HfXftEv-,RDvmP7zuum+$LEuRs،0GoLjnN.#h$0A1YG"Q?`fgUP +ha5"bA($q쨊Gۋm*ZAD Ɇ5 dbYV^(L]ۍ_VڋFNѥ[#{敝,QKǸ( ( ( ( ( ( ( ( ( ( ( ( (>4n<7^ZI INW,K}Dpv6HP@P@P@P@o;qMz8A4W:68gVn\m!Q.7e. 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7jsp^/ rWS0Q,P)@P@P@}O-5;uVX.?WLz(7m `=팟f0;&  (+ _Vʣ:?K[ ӿS@) Zw*h/o+NM-?i_;4—RT _Vʚ?K[ ӿS@) Zw*h/o+NM-?i_;4—RT _Vʚ?K[ ӿS@) Zw*h/o+NM-?i_;4—RTš,u10xe[G u 2;̌7FA(@P@WYdOs#2 |2*%s+Ij*>MM/-.*—/|$,j;aAd=Q#HJ(8`8a88HB"t%P@s:4mR^dw3[ݢb)$s$ѫBIP$$^\Z~Ζ'> x[#?_<7Z~Ɣ>B:cxwLflvTsqw2![qÆ@P]DhUtudtu 0VVR0G96ԾGP1dn8wa8Bƽ RT|%K^(Z ( (xO_ZZPO[hۜI2@f@?-P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@iPP@P@P@P@P@P( ( ( ( ( ( ( w?7jeVR+VR0A 2#ps@&|BdԚ6mF{I%m%lJS1)h ($n%#yD(RI#TDE3dºO@Vյ00FÂo1IFX ͼt}9zH!//m\$}@(%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ɠNosmyiqf9lvHِ O@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j)jug>[ͥluVR>dD!u|78X,q2&4$_Ʀ&2, ʌP@ZtZeח2t 9EWH>/ë_ u7[ e;eeFۘUFP@W ڐ2ag!gY-$`jˑ@ 4=IJO(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO зNommgio a8"\v8P~O@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( (bpII'jMODŽ뇶I/6m2[\ڬ=$ ;G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .S:;=Las(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)b= kJR׭OmnF۫bmFmx`ˎ3>; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( =WN;B=.`׋i+Z)2rNX*`|M`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPx6mbzll]+eSaq&lP ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ];G{N;d3csc!#A&;bFphҖx<9`*/#v rL}i S@[x:yj%[9 !8b?yK,6sݝ$4nU,k=Jч ^\@G"΀;#⧋4UV-B52c[|pc:=tvKԟj;O.w?it61cC;uzP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j9Oo.ϛq.䱱Fk ˂"$m̺6!`n\]e[Kv$fJW5j~JSC.G _eq?+O 5?@%~)_ƣ?28]˯QWuj?~JSC.G _eq?+O 5?@%~)_ƣ?28]˯QWuj?~JSC.G _eq?+O 5?@%~)_ƣ?28]˯Q|Cn:J5{e#E*R3\G2{¾&Vj.Yf|lC&ѽd#E([״Y5tЃ5y$D6, j.^FDRnYx~[|,[G(OA@w0%)w$ޒFx#[12Ā΀8OxƗZelDhW;N&6eUvJ#<@7xڄ4HKgT0@_{>f'%(Wuj?~-ZxŶrxUvSwrD}IdRiD"}ʆb6x0HأpzMy>$h^RSeM䌅s'=Ed"U*X٧0Ky2q{{N9`8H:nawi8K>:@:Z7"+I!3N~iO *lMi<( ( ( ( ( ( ( ( ( ( ( ( (+̏=iY$C[>[j=+O 5?@%~)_ƣ?28]˯QWuj?~JSC.G _eq?+O 5?@%~)_ƣ?28]˯QWuj?~$+ĉ=mH KΜz4PAh|KWCf`VPD s{c ,QGcYJ<@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j ׶m+[Hdy8퍨,UT!X?ƧtJrʅyl8̙Pz ( (+V.> |łaLwIg F we<ĐxN(ш[b`?5)mboJv ( ( ( ( ( ( w.>L0\2 򑇡r('n>LO?*.5ȐCgZc{s^[_ZL"t9]Uв0*PeZn[+K_#/~T>:@/|XoDJZX[t@s+}XPP@vdђ<ۗg bpGBÕui4? 8H$DrM29\qh$YY]Wi$ػ#͖gf%$(׾Skh٭5X&u^ZDgY-mɴfLC%n$5˸Xֺ>q #؆PGҀ> (oIFز[jbkkve_Eެv=` }3;%ӺU$ ea&a-">D4Pu ];KwhJ|5;y~ D2>q"i![(b=cYhzPP@IA,sD%D7^ IF`@xBl1HF 6? P@P@P@P@P@P@P@P@P@P@P@P@P@ j^jԬZ[ɝR\H ( ( ( ('+-Dê"0 4PaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7jͮ|5&Fީ-ʼ֋C|@P@P@&>XjW .̏M ~Yi!o\hkm]C.:-d<7pϹ&ky 4(8$HxGX7Rv:]Xe# 1@ ^?wɬeҠR0ywaRFI&ӘT^!@-!R]hJ9-$SƊrYZ~ ( RsXmՙ}W{2gv!@0>4hD2 t[g|Ϛ=4h%PP@_9g+cot˨\D \ppVEH^i6]gO,LZ({B~X-нtEO z 1@")k_>(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE@KgWPc~e!@()?b-Zx'ŷ1x{UFcv=kJ>$R^γwG&;}`FyJ%8"4zMy>^&IM_eݘM qWFg %e '0{r6qٳs{bA8(@4:naYaGiiE~Id-#,I,kۍWQ-R<`#g;{O0r@Y$D%MXR1StIu!e?PO@ S?-k'tjI4{ ιO.`AhXO$}?ޔq@QiZ]ZilD ˷%I#4v ( P[ky/ sA2D=NG\zˆj_M3Ubcߤ|؜m(@8Y&q"7<7AЮ#]^"Gk eK*UPUu@!lI\WX2[\G y,ᔜIԉȏ 13l(h( ( ( ( ( ( ( ( ( ( ( ( ((S?-k'xOеQ)?bE(S?-k'xOеQ)?b$+imY˦]4IRh|4H\bZOGYqAa Q4OdiB$@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j|\*!nۚ{2:ӧjh ( (  ݃u˵_~ŧ737@@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j!Z?kѐ@]4"8( ( (0*ox$yeS[P*A( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (k ( ( ( ( ( (( ( ( ( ( ( ( w?7j~4hޙF˸Dx l{I2x[Q"<6 ( (' Kt2Os4Vƽ^YG?:Ht?L,,Cca5G+ozѠ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ?hpxFnAsa61%þU_1A/@s@_i64V<3Fzё7\+*J( (=ZMy%C JK8CGiS}Lq@F@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j<'İii떱p$c[?fSb#1˓5[E$ʍȇ +)2=@P@P@ׂ|{$l ki.p͆I&XZYi֖6pI ¤h0r~3@h zes_JZZDO+sWwb4\ʊ 0:ƭH8U/\\H\ aɌ?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P .S:RZVk\@wR>FIf!VUvd1@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j |WMheM-J *\JKYP K ;j6k/t{_[.d@)GP@~j }2^2м)AXdA݀ɠh Gw≂ Js}wiA٣gf {ͭVEmmX.{$ (>0}l׷_q@-P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P}웾hSێwyv}@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( }CKj:Fy!,֐O3kP: <#nm 0sRq{ |B/1>Ҳ^^< ~ a("_1Q(~t-P@S,-5K+>%&xuWRA BΏjLk}5=9 !\S?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR(UPx7m"u[ BeJ.s 4q,M (k ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( (0; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ( ( (<ƛsJGVs_[3"Y -ȝLh*( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ۝_Tm{][2LY𨅻 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( (R2<A #zMO?HƑ-ΞdE4{e7컣euUTXxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX v:6c}>Lll..$ĒۥGݱjBo@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j (?Qմ&jwֶ0󆹙".G%bF!~$A<-h,u6nz0b q: 7#z=v(϶{|6inG4Kum- E1;#XX$UyYӮ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j@9$@mz+BS{BK?,iA€ N (u 4q,ͨiT6y#"֓Z0uP4rrNxoWlצwǵn%ZHf@H*>\^)@!X:HP@k>ׇ%kkƚp^?G@!!sJ!!oO_<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-{FMjtk X$1jq-E$;Vv;c (+[w{q Yc8$P>fbAcKu[S:%vb\tx[|5ӿm@>-FUW4(Y'3\^y`QG.@E,SIʋ$R$rFrnсYIr: (9|p;8.yy)Iف0~3X>ï6;]?gTS-z]?U'`O=(|;V0鷸 \!Y6,.m ʌn (42"DEffl*K18Pj_<#G5J$;]\ki#no. φw-{^EI",Ԟf_vRdho;Xto-fx:68e$rxF]UU@P@P@P@P@P@P@P@P@P@P@P@P@P^izo"j.4@'ˉH (K‘DPHEihP@m(?] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<-> "I#p) %(CO!'4N붢Jܝђ'|p .TFA PaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7j>|ǒY3Vwap|N1۲2d6hP@P@kzޡFKMO<_!rrM.$'q畦-Y y]#P/t 2\!{%w!$hY#q:0tſ\_뭡#.{a3 Lrݱf\yP@{\ H[<^$h>}s} >rK]LoX-+(iY"wPJh^%ďqw5c鶲vGd TɁpĶv,I(Mֵmu59~V9İhfBz4NPL!,#>#H $m.Tq4txU]EfPICOv€>)Ԯ/f{ZYӲ*"QU&xnx.-Ia&+$r! 0AsȠ<'{L՜*( 6ȋG=h㹼7ZV5{eYfDuF5̭ 4F7zvm[LFvb1 0(ozLJQ.-.$i2@2I#na eWe  +aE heC)P4񧈮.P?buU;&m ZO&7Xh.+H>v~6=y Qݑs95;ȇ_7_39;Ǟ=( mC?OKP_>Vv_hq}ߴyxݷ((ؾ yj[wyyvms~;nzω_$kNVT P@X|'!vY}}~9w&ǝsOuݝvqo͚(|?v.ϓI~+om şKbgy@oSF GӅ}@P@P@P@P@P@P@P@P@P@P@P@P@PP@P@P@P@PPaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7j~1um-[0Vh]3H[Lg8h ( (A 8 cڡּ;jlۥaC.1Kn4@g |G+Q8Ŷ2@<41!D)b@:cF 2Ws,txJ]sE`Hcsύ0u>)n,oa{{YZ)aӳ+ 2:]  U&xmmy.$H%-$9 2I$ɠ<'x{L\,.w"K+ƍO(IIZu{HfXftEv-,RDvmP7zuum+$LEuRs،0GoLjnN.#h$0A1YG"Q?`fgUP +ha5"bA($q쨊Gۋm*ZAD Ɇ5 dbYV^(L]ۍ_VڋFNѥ[#{敝,QKǸ( ( ( ( ( ( ( ( ( ( ( ( (>4n<7^ZI INW,K}Dpv6HP@P@P@P@o;qMz8A4W:68gVn\m!Q.7e. 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7jsp^/ rWS0Q,P)@P@P@}O-5;uVX.?WLz(7m `=팟f0;&  (+ _Vʣ:?K[ ӿS@) Zw*h/o+NM-?i_;4—RT _Vʚ?K[ ӿS@) Zw*h/o+NM-?i_;4—RT _Vʚ?K[ ӿS@) Zw*h/o+NM-?i_;4—RTš,u10xe[G u 2;̌7FA(@P@WYdOs#2 |2*%s+Ij*>MM/-.*—/|$,j;aAd=Q#HJ(8`8a88HB"t%P@s:4mR^dw3[ݢb)$s$ѫBIP$$^\Z~Ζ'> x[#?_<7Z~Ɣ>B:cxwLflvTsqw2![qÆ@P]DhUtudtu 0VVR0G96ԾGP1dn8wa8Bƽ RT|%K^(Z ( (xO_ZZPO[hۜI2@f@?-P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@iPP@P@P@P@P@P( ( ( ( ( ( ( w?7jeVR+VR0A 2#ps@&|BdԚ6mF{I%m%lJS1)h ($n%#yD(RI#TDE3dºO@Vյ00FÂo1IFX ͼt}9zH!//m\$}@(%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ˠNWoti~J-C.; _e?q?+ 4?@%~@Ɲ28ӿ[]w ɠNosmyiqf9lvHِ O@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j)jug>[ͥluVR>dD!u|78X,q2&4$_Ʀ&2, ʌP@ZtZeח2t 9EWH>/ë_ u7[ e;eeFۘUFP@W ڐ2ag!gY-$`jˑ@ 4=IJO(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO зNommgio a8"\v8P~O@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( (bpII'jMODŽ뇶I/6m2[\ڬ=$ ;G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .S:;=Las(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)b= kJR׭OmnF۫bmFmx`ˎ3>; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( =WN;B=.`׋i+Z)2rNX*`|M`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPM`撀ퟌ_*h4lbS@?g %?T?&i(B0I@/4 J?~1ЩLPx6mbzll]+eSaq&lP ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ];G{N;d3csc!#A&;bFphҖx<9`*/#v rL}i S@[x:yj%[9 !8b?yK,6sݝ$4nU,k=Jч ^\@G"΀;#⧋4UV-B52c[|pc:=tvKԟj;O.w?it61cC;uzP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j9Oo.ϛq.䱱Fk ˂"$m̺6!`n\]e[Kv$fJW5j~JSC.G _eq?+O 5?@%~)_ƣ?28]˯QWuj?~JSC.G _eq?+O 5?@%~)_ƣ?28]˯QWuj?~JSC.G _eq?+O 5?@%~)_ƣ?28]˯Q|Cn:J5{e#E*R3\G2{¾&Vj.Yf|lC&ѽd#E([״Y5tЃ5y$D6, j.^FDRnYx~[|,[G(OA@w0%)w$ޒFx#[12Ā΀8OxƗZelDhW;N&6eUvJ#<@7xڄ4HKgT0@_{>f'%(Wuj?~-ZxŶrxUvSwrD}IdRiD"}ʆb6x0HأpzMy>$h^RSeM䌅s'=Ed"U*X٧0Ky2q{{N9`8H:nawi8K>:@:Z7"+I!3N~iO *lMi<( ( ( ( ( ( ( ( ( ( ( ( (+̏=iY$C[>[j=+O 5?@%~)_ƣ?28]˯QWuj?~JSC.G _eq?+O 5?@%~)_ƣ?28]˯QWuj?~$+ĉ=mH KΜz4PAh|KWCf`VPD s{c ,QGcYJ<@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j ׶m+[Hdy8퍨,UT!X?ƧtJrʅyl8̙Pz ( (+V.> |łaLwIg F we<ĐxN(ш[b`?5)mboJv ( ( ( ( ( ( w.>L0\2 򑇡r('n>LO?*.5ȐCgZc{s^[_ZL"t9]Uв0*PeZn[+K_#/~T>:@/|XoDJZX[t@s+}XPP@vdђ<ۗg bpGBÕui4? 8H$DrM29\qh$YY]Wi$ػ#͖gf%$(׾Skh٭5X&u^ZDgY-mɴfLC%n$5˸Xֺ>q #؆PGҀ> (oIFز[jbkkve_Eެv=` }3;%ӺU$ ea&a-">D4Pu ];KwhJ|5;y~ D2>q"i![(b=cYhzPP@IA,sD%D7^ IF`@xBl1HF 6? P@P@P@P@P@P@P@P@P@P@P@P@P@ j^jԬZ[ɝR\H ( ( ( ('+-Dê"0 4PaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7jͮ|5&Fީ-ʼ֋C|@P@P@&>XjW .̏M ~Yi!o\hkm]C.:-d<7pϹ&ky 4(8$HxGX7Rv:]Xe# 1@ ^?wɬeҠR0ywaRFI&ӘT^!@-!R]hJ9-$SƊrYZ~ ( RsXmՙ}W{2gv!@0>4hD2 t[g|Ϛ=4h%PP@_9g+cot˨\D \ppVEH^i6]gO,LZ({B~X-нtEO z 1@")k_>(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE(S?-k'xOеQ)?bE@KgWPc~e!@()?b-Zx'ŷ1x{UFcv=kJ>$R^γwG&;}`FyJ%8"4zMy>^&IM_eݘM qWFg %e '0{r6qٳs{bA8(@4:naYaGiiE~Id-#,I,kۍWQ-R<`#g;{O0r@Y$D%MXR1StIu!e?PO@ S?-k'tjI4{ ιO.`AhXO$}?ޔq@QiZ]ZilD ˷%I#4v ( P[ky/ sA2D=NG\zˆj_M3Ubcߤ|؜m(@8Y&q"7<7AЮ#]^"Gk eK*UPUu@!lI\WX2[\G y,ᔜIԉȏ 13l(h( ( ( ( ( ( ( ( ( ( ( ( ((S?-k'xOеQ)?bE(S?-k'xOеQ)?b$+imY˦]4IRh|4H\bZOGYqAa Q4OdiB$@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j|\*!nۚ{2:ӧjh ( (  ݃u˵_~ŧ737@@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j!Z?kѐ@]4"8( ( (0*ox$yeS[P*A( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (k ( ( ( ( ( (( ( ( ( ( ( ( w?7j~4hޙF˸Dx l{I2x[Q"<6 ( (' Kt2Os4Vƽ^YG?:Ht?L,,Cca5G+ozѠ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ?hpxFnAsa61%þU_1A/@s@_i64V<3Fzё7\+*J( (=ZMy%C JK8CGiS}Lq@F@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j<'İii떱p$c[?fSb#1˓5[E$ʍȇ +)2=@P@P@ׂ|{$l ki.p͆I&XZYi֖6pI ¤h0r~3@h zes_JZZDO+sWwb4\ʊ 0:ƭH8U/\\H\ aɌ?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P ?.j?tx|4m@.φ-?Q?G@]GOh ڀ]) u[P .S:RZVk\@wR>FIf!VUvd1@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j |WMheM-J *\JKYP K ;j6k/t{_[.d@)GP@~j }2^2м)AXdA݀ɠh Gw≂ Js}wiA٣gf {ͭVEmmX.{$ (>0}l׷_q@-P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P}웾hSێwyv}@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( }CKj:Fy!,֐O3kP: <#nm 0sRq{ |B/1>Ҳ^^< ~ a("_1Q(~t-P@S,-5K+>%&xuWRA BΏjLk}5=9 !\S?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR)T _?Vʚ?K S@)Z*h/?+QMS?j?@G4—OR(UPx7m"u[ BeJ.s 4q,M (k ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( (0; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ( ( (<ƛsJGVs_[3"Y -ȝLh*( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ۝_Tm{][2LY𨅻 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( (R2<A #zMO?HƑ!x3ZYIgAxúKkeW}E5q\41Ko:4nP@PsM O=ı ctM"EkʢG,@;[KwVp(e+èLRW9=EX ( ( ( ( ( ( (..m3Ak 5ı;T$d@X8h;? IL8u.Y%cPjl"LXU,p8PI9?aҵ w/į|N4^@5.b*WGCx?[U<:j.lF-o6U;'N<ҀN_%| :~^au?wZx >j͕wo@>Gd3|\j3i{k4ú74hHyo@1vYG0h@xn?jߵWƶ2 ]#RVҵk IP:7vyrַ]uk{8_{/7(ր ( ( ( ( ( ( (>Id~۟ωgZ>}_ڼ",luNO kZ1Z,r-#vd(4~ ⯉Y> _G4^tuoG'iʹB2kmO~)7ōkៀ [xWwVZ'E⛽XL3XX][Ck5RA3j1(+*SZ?wl*yx|Y_}W4ɪ[O ֙ckzPX4Wnw2~_>}-t?62jĝ1ҠIwhjރcAk Xl+ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (>t?u9mҵ1$x'g &Pnm{=<ʠ(gJoٮnZ[IO|%5ޝ6 5☵u\|>ȱ zΑo5M;Ct+Śηn_ږj7CiaiPOw{{u,VֶK<$H@?8o /F'4; yχuǂZ=լ _|KXHO7Qo$2~1~m/w\h67:4ѪxRCb-"Úև}:n.-jZ|gP/ ='Moo&|3K{/ NDwSw\kxfk hqrFR(&xC%?-Xx;CE4}e>|7c?lk85t?hP4 +]g6Z.v3XnPC;~?a_>&67Z>+rzⰵRڎgo-H泊{PԚ( ( ( ( ( ( ( ( ( ( (?( 9gO]?~ JW?g??KAO\ou f6bw~'oihpGyOt8֭Ui5/ʑy&y@?i?Z|B0٧N;[0xXx&Lzw !nc߈?_:v/i/n1A?5-MVH -^J4,S?(K cfG'^^s=^ T8[\ꊐWB/Yf[Nԯ/RS^2|os/A>Ӻxźx"Ÿ5oV5wג̚}45ksVGo/5-=7{jvLJ{6+ٞXi{o=+eJO?P_~ҫ\| 1U|GZ5։蚌Z5 f>Vqjv uwٻF/%4}koϋ,mjMs+i|'Ah[0{w|V[,"77 QFS~@P@P@P@P@P@P@P@O#ggb/9|,gO7O괿3 燼YS%.cY}K4<}sn~ŤoqSB)X+P^,~W^$|Cs *t sDui=A|I HM6dn B+*SZ><|aO SX|ic(S٤x↡wᏉ0ކ 4T|S6j^6K[8`tWট_ ~&ŸWe%֠PR~"O}%x.onn ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (>tG؛ o7n=5[I;I|3_ Zlp2O>$^"f,㕡VN7`3_{f{7'û8'c J_@HMo+|~+w?Yl 3׺|o-5o|" T"Oi^ BWpַ}u~>} E?/uψ)WjmuOXjJ*x-.tKMl C}8jٻF~~_LJ>Nj|M_H=>'O>Pχ4 %@^,~W^$|Cs *t sDui=A|I HM6dn BCџJL>3<;sqŪBm$K[tg{ ( ( ( ( ( ( ( ( ( ( (( |L'>jE؝|Q^h̬ϧ\F +X8?? ~'|f _F桢S/YMgv-ûuiO\xȏGpͪ46`/ſ7i>/|9^.<7i|x%4m#^ZJ{@ Ysǐ6Ե-YumλU߈nnO;V?#Z{'Ej6k]&Q-4ydTF-~ԟe_(xڳ? <+Q׊~"^xwF<xUwzRӬ#F|)xoͽ y~)~K⿋x0P@P@P@P@P@P@P@K`|5?.Ax>i_|Bu^M id7ϧ4M-K~0.(ggqEk 5]MrDz  ⴞ4伝eh&[`ΖJ,_mqoyxg΋? OuG]>~.֭-PMaWIo4kfQj "Gpgݖ5߁_Fs9[i~!hSxBn|Mx_95Ww jڵz6CR{'^ ? u5[  iu?Mr_,^1"%SnG7<'Xk|z^>YԼ{C=nu]|m"? xD״kVj?,kz~ π<;KĆ}N׾#׼9q}>X[~:mr[ ľK<]r\O{siwū\zL'ĺP_V?|gp:  ZPEF&}2棨jut_Fs9[i~!hSxBn|Mx_95Ww jڵz6CR{'^/#{~|4/V?C7?d׼q!x_~ ]fQt#:eΏ(9?m?ڟWgoƭs~6-㯈q{ k'Ě>35 i wGֳᨵǭjZgu ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( <? ICպg>Oy?n@m#Q=G6T_(?GQH/z?iE@m#Q=G6T_(?GQH/z?iE@m#QT[K?xNCH-Y`&*#e(̌E,Ut |K?x9iwz΍i{ϰ}y:6߿o̰a}~P@P@P@P@P@A3LvcoI쿵?,7w"Z|~؞^>#8wPǟG6T_(?GQH/z?iE@m#Q=G6T_(x]>hڍSGs`MTm.`{7Pϊ~ 'Ǿգt-V<vĨo4}b;`Ωsl][Ksesmu8P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@|yAfg;¾'/OK> O'_O'N>m_G6T_(?GQH/z?iE@m#Q=G6T_(Կg /x5<{(oӚXX$t Sh]7[Ne{eZj:uݵm]YD]\ ŵ.?e=_ǟ׭oq 9-tAŧX]+j |?.-Ζ~^H߉,3 ( ( ( ( ( ( (!mnbImF{{eVhf9bY$Fdu*H`ɏ_ |{ |MSZ3c )+/ VUеXA0:ͳ++Eum-͕͵@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@/ğc;ּ=A%ťdZmmc)YBDH0y";//u)'>#%O%|5'lnA#Q7Ok,yL4NimYXóHnzӣ~,ௌ |{-Z=WBclw]JG,ûj 6̬մ6W6Sz%P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@p~x;׃xGZ<| ki@y:Mgy E Pox߰`<?0'km:٦meco#iWNS⟂2+Ih] Uw*Xa3\;2WV\]NP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@Im^<  k\I\Z\&Mv25$I )@?1 ~>>?POBq9Y|e"-i<{A-':A-pe{eZj:uݵm]YD]\ ŵ.?e=_ǟ׭oq 9-tAŧX]+j |?.-Ζ~^H߉,3 ( ( ( ( ( ( (!mnbImF{{eVhf9bY$Fdu*H`ɏ_ |{ |MSZ3c )+/ VUеXA0:ͳ++Eum-͕͵@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@/ğc;ּ=A%ťdZmmc)YBDH0y";//u)'>#%O%|5'lnA#Q7Ok,yL4NimYXóHnzӣ~,ௌ |{-Z=WBclw]JG,ûj 6̬մ6W6Sz%P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@p~x;׃xGZ<| ki@y:Mgy E Pox߰`\x7{GAu`mmq^XYm @6w=`|MKmsz]h4{O_$i_OZ;e/Xjt nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\R/-,-mnmth YvXVI$P/x}=+7Q7/m#}NnZݦ]`#m3Q[ `_<{_|˩蚚`Hc/T7zNh'uKuKwfVVI綸۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴O|kǁW[>~Ngki h/,l䷹fg{ytwX]O\??q|G(۴nrO\??q|G(۴nrO\ҷ.aI0W݀YGU8@P@P@P@P@P@P@( ( ( ( ( ( ( w?7j`|[pv& $gE/_ eسhZ˧oqv?|tү<;+1/Zxqe3e([}y:6zdo> ( ( ( ( ( ( (!nn$t196hEd?/o~6~17߆K, JlxvFI :v:%ˣ~2smQ5MTL;c/T7zFhTgTvuehmP ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (8#S:ag).1)'hq6_heOlu > +9⹴fytfX])` 4P@P@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7jiكUng}xK㷄;'+(|k E-5b͡k.%ƚ~JDŽ/HIi?]-5W-nھ|ۯʀ Yb IQ+ O_΀2 66`L៦(ݶ^zvOAmym9?AP@P@P@P@P@P@Cqooyo=ZC-͵I=żc 4sC4l"1GIg׎b_m%esnbo Y3[, hHtEntKFe7ڢj&*v}_FnZ3=bΩql\Ksg=̠P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@q?>G⧄uoxIX5vM  m ,+)iw'X${cpe{g}յWssiyismuks 47,3D1VRh (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j>f Wźm/hH6q[(k+[Z6|h_cۿ>,xIO W6gtVZ5m-΁z~#C%]\xMC&ˋ^y˄@-^¸EswjW1c-xn7RhImhtxُdI洰2  He`YH*FAdF Fhh ( ( ( ( (!nn$t196hEd?/o~6~17߆K, JlxvFI :v:%ˣ~2smQ5MTL;c/T7zFhTgTvuehmP ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (8#S:ag).1)'hq6_heOlu > +9⹴fytfX])` 4P@PaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7j8_xVAP%ίv6'jJ2PCH8ʅH?,~=#ּe?V;T2孭B%[(yw)ԃY$)}Ɠۍrt{iKw<x$sܷٮK7@P@>-/UKFۤK,A՞'3[ ?cnDPqo[NŐ1f\(b%lr)n;5}ie ˦78cܾlυ](y F-?ƽq&HҢo.&4wO=;zo]:QbiDׄJUf%PJV (~#O Caα}H$[+]LlBH"EI@>cNVP%gR2s QFN@Pi}{a*ewsg2kgB$[y&Z"}س;1f%RNI?_$Ph<_xv9loh-ď5Ug*,F9W?,&>k-"VcCB3)<GSBB έXhZokng 31EIdorITK]Yl&y;YㄑI?ѹῌ6=4Pwk5v8)wop<,ʤA+Ƞ A9ApAxxWFRӳ {bp'XcDQ<U;|[y=BpY-č 0ۻ #FPIHd)^7du>~#_hYH-HRYؖW"2f,+ (ѴP@P@P@P@P@P@P@P@P@P@P@P@P@xď׳AkQl|GPim d94h,Ng𙧞̷KQfմxPm-໴&HghG,2$rF̎& "XI C"VHu# 0 8*"m^{.`rX%bxglG9B@K?4( ( ( ( ( (?( ( ( ( ( ( ( w?7j cU4VBҰ4Bɥd.ֵ{S/|R C)Asvf _;zg;PJG%%~$6k߸{6tS}-IS]'W 4#4y > ?ټIKo\n= (=YxZRVIwsA*subJ(ɾ2RvHzQBU( ( ( ( ( (9)`(xHE)$RऑȌQсVV*?{_ m>!Ďq?V,XLt@#ɤix'&e`O|@3|m@{ cm}lhXUhf=म5fO&7H;Y}h.>\cm62rEg; 'N$`(@C,<.M ,R!,l7B0C#AxkԴ+Q`+Q^)GN|ZWƺ3[F 6PTz2iހ< (+-%K^; ?ǥ};}Nm#š;UᢒxU<7bcr@@SK7k[?/i ͏ƱM9ȠM+M+$Z&*08*NAP@IBb$Vހ[KpS|~5jshሶEy԰aٶp'(P@Z.Լ!f.O}4H,[o

8}\\r7bj1qahli+tݏǴݳe""2W\ՠvVZ[[ۂ2ڰl߷sPDz IDATXz46 (v<Ɗ|VT7}KO]"?Z^^\.G.#ɐJHR$ m faw>UYEl>b2 *hYKHG%|G; 4: X$}Zq,xkeIObLr4p[SN $y⤻{KJe;.`."3u#ѷU%Wv2b= do@s@@h:RR)S1RF殃4`W>=iP4/ʐ;bFbAe(E0)>`8FډfT HfN E{5P-(($5GQBJ31Dvb7.k: _oc!e)ڷ\݄sX"#`) 267E(eXnDNK Huߦy 5_.lP0Qo7y&%JC2s-l:AO G25`&~HP7p'j&Z]A& 1^lTZv%$BauyOvt|gl s z&](ydStd$xc {S:aj@DܬH|%dC_Qf ՕlR?+ C8JGL?eC)(b ]bX}&v֡պai/,z2veR%SE6~&Ѿ2(Ic zʬH"!DDAADDjspfV!""""" TBAu_q[ܬ$rBe PΉOP%P}L-Yg"M%T%`I`,qAE,p4tN#v"y(M'aKP zgPD7SX8ag(. 4fiI$)(MlɎk#"]2AEDD-t#: -Pt:i$q$\NlBi0]Xۓ‰&o$u{J:(*;-k`̣ZQY tCRFFC,\?WD\R%{QNd;Tz2|:sC_X&f1 JLut@eMc 9>p.KSCu`T}=D깅,o% """ KKQH/hmcjl2?r1S: DrVxQ@+ɀRETNB 7PڳfӔhAJ~d;zu, @2Cn%DMEɖFP7I,h~(d<]MLyaX)> Dž&atld>e& o@лm v+ ı' 1koxSIVyY #&Œʆ!Lk::jCQ),46R# RL*ȼ~v {FIDDDDDKadFČ2?+cӋr3qYp ސpQ bT2T20#H ~%MP@Q2/0>LK EނǦw-DЎO"B#H;>cn=w짆3XA)Q߬B{h*J ;(;YlH(>]y[ UPTG}ny޹i(ԗPNU %j: *TAfDI" n4@EZ?STKrVM  'ѳ-Vŵ.1Ae7b( pO5HR``R 3f8wUUZ]IC ab'."3b˖& 3W@.'Ы/z2MtCQV ߱óAY#.W\9CprYnN0-T5Tah2pό~wع%dHn]n[i2+c+{+W;scG b`%""""2K\\.G&!JJH$$ }OHOLU X6NHL6Spa?Њ} 4mDZq\q e/[.=o 7C\9'3w\ܔnbK#t2pCLpfH/{H57qd%ʬ@F:(""""cD8F!rR;RDXJ1_~佼܍<1[ ѸjH|(G7v-/2gvv?aYܯNm|φ a?!w|rR_Ü< 5gzV~ĖvOI7]^[(z (+åL6lڭ~vO7+> p,;0j Hfh&lȖwO8~\ʹʖ.-Jyx÷w#R\LwS顥nq ?"(6DD4̧N|!s]wĨD"?#HM@3zX(&` !zTBU= o.]QV7RklƆ;:y\%yưC*Orܠbi8MM>ʑS_I&O&4[XE[RL{m96-1 V$ΕC}+ z%1)=Ef,CookN=dȗ+1~5;)\o% I2:3}kd*kǨfnmj\8pSAZz ku _8I/ 7w/gG}Gy vg MV Q÷O%[dK:YN@D=3.sOh1ހPy63{ϣplLc/b t6s}Hh t\o\Kx#eSa]ƀS;:Q}SXi<>aԶRr?Ϧ MyMg38[ V}02yàa %!/'?˦{12a"A-e;NE}VM4S&δf#f}vЛń[>% ?;ȩV\Ɠ0ZtM̉B(YJ|$Fv72rh߷86s8)T=LS|y[Bk1'߻S-yC q!"@PWiv@IQ/(?Q ͭ(VZԜ:qe"""sϖۏéd|drM-}((> H؟]eN#Ix!#rb^’Ks'[HI$?HPM.Eb%~WΞȒ`W1J]X)>if΢P39P sn0u0쬅8TEl#$TsԢcsKXl"&ZNy?!: r*\Λ 7b&ߋ.$uGd,0NA%%J&ǒ0)AF JL }܍I$N ŭIp&ő Jʭ\FcS:΃V{DPDXTETAhhO.$qPyŎ]R E9+)v' ASH^ "+DȂ9sY0AM'0a"B<A$ zv gʛ+R\arwwK0WJO1g?*4q9I31[On%ay̚;,׵sRә3wsŐMi b/dI5/ADz29s1k0Z2|Bf]U|[tQ\DLycΜ ӎe ì }Ýe{R>3J=-D[>[}!'[1c<$OaLS\}VTa0xp@in6Ma';\ėg_̜x<*ZL<}9sc-C~;"'ajt? HCDAHvb܅оx]m[- 7\l #bi[ 8uI]y(ZLvs葱vm[SNaQ- M"F덫U0vŪ2!޸ Pc3oE1R@OԔWX)al5`v-rRF&mm@O ^R)`鍎2+~sb' Мuӡ~Qq輼' [k0abx0O+jV?f唦gAilJ'48]xhϽ qT QǻhA6U%Rw3u 2cj҈I၇ 6V8 e4 HPAd2 w={ʔ%Cԟ8@PgBq g4QSeeeԡMt]jO42TJ MtSC; 2FFʳw v- S)Vel}N] sA?*60L f-H\0SSt T@G⊋s+-;{fS]E>5W˭M=@{ڡ r`8@ƾ PRe^7 e7HGuAݧl5#?1O`tg"mUB7.^ᥢ 8y1+1ӛO]+ z_;Iy}y1a^RWtm1K!SPP9hli챨҈0Id5[eT 1)Q\;Nph-llbϪwf8&z"hiVp/\&H:')Dw_(^Nطo߻ƦԒ2 %dH K JhS|Jvw2s2"'l|&5 Xs0XJ &RAC JG1(PY1 TQU' ,'3ȲGL-QrfČdB=920@pU%#h'!ÑM1]F1Yh]S$fG`Rf*jJkEpw: Pu*Y[UY VtlG{8%H-%e8y@ $fE륛2xb.R/^~z5]pzIrSm_u_*k/`JWU~4:W.WRe_mB|\NP-)>%X_>Pxw>` IDAT-mVε8.rp#tA>0.;axn\Oٖeળ2 V*doH-4+73 0^@|:җ#HI +tu2 4Lyq.5l~n!zi::WCTFD"%~s:i g:VxP)}/H! bl HjĦIi$Jc.V^x2R^DDO :f\^wH q!]9B1&W "vo/0f `g._#3)PG 0*9.6ùiyy\KDڜnhkkCHxݎnf:8‹4XH+uNԆbc&*HŅ9ؕ ROmDӋ$E48~¸d̉ v<At`~ '>66)q;,#As?J8eR}MPy'@?>߇#>jFlXƞP+5qwG& v@Ő' E{2]vhXl~G_K`G:d,: yQsnES˿ǵ#al /<}1gn#H1DUgo攗>#Bw߱askܕ qk[@p 'xf|Zeco˃ 7)\eqWz`~d }\:)#D{ ־MJv*\l:F.DG%[g!HvF5cfZ118I۳Ց$]кofΔ"fMR e)43RV{TadٻrwwK)-@\J ݆]le:ڰU205ؚ0'kO&ZI|)eF_$̕4%]&wYT Gnk񺫆4_RǺ7R;yEqAO!AOIAsoֶZ _]3욆K^Tk Ʉ{H~5}Fw='F1n"i>e^X6wm62+Vޢ!App8:ZimmE8LX3?TTp ~~̟a7p4|Ar=ܣ0!tf$B^85š;rkm+C332j p 4#Gw"iGrbixn q\ɆB%AMX?O 4vbKoYzL[)eRts/3)8pP' ]Tn5ظ0]L Vcl İK~֘l$?:mJaį\V20ZfysǃP`(-OUTt0\ȟ~*lT~Ku@bӘ~2FΎ%@ft[Տ>DE)DN@n+#cF&ei>pH9W2(*۫LX6+ On:/|UGQPO+̅dJ8F?~ЋJep{cq*xSo1v]fmx& EK)FB_Aw9UDF)R"_Xƃ:~<\\\]ÎH[TzlNXW5&|R }+T#3#d<Ќg4=P0X}>91KƐhۃ+v`RPcBUV=xpFQ %RO ucwVG0B4oCl p u|ڙ?pZrϒwZǩys<#m"j7oNbCuCb)ۓJvy#pE{XfFxC} R!W(=TlnȌogMI@+iֆ PDΫ׻z.d3a+J()*:ǩ_p*(8D`OTGN\ .Ĵp1X ,X<}QHOC+P3qy[NbTsa VeXdWGN$JߝH Sp#r >GO](q֊"{Ӣh6R"_]Xّ3MT߁%u]:4Ҍ(ϞB4&t?j 4:)+::B{d軝{ɽ_ˍGbנ 5Ci>×_2zip3Z½_sWD0WL wQA O01ψAźZhua.)aD1Q!x:TK')<{1X$ꑷ~?gw?6* =2 u>IФTgo{ b{{b??p!,A^6`ح n:nR 6h@ ܑϨkG]s:ԣl;AI@F+΢x`._-Ro|X>%g :c.(S5 D6uב\uuTRy%&0Sq = Nz: f1O AO&ER&* -ScIjæXJ]]#V"U6wALwSծxY)wh (fiW?>x7R\-!#.nL.]mLx<x*ؾ뭓 )rJ΢2@w{_Yˏ`E^4^RfmgNYɑ;UB.fD#3; 166PYOb>]M~/zc|@Ӎcnxz }XDC|F/<,ۏǥ0)Hd}¦ND zv~>.ؤDM Y+ y< ]@8*)ؖ|{M1GA|Q3l-ބNj.ע볃7W3P%쉱Ô0n]X rK1?[,Tx4'ƖɶZo>L6+EOmӯbH9F ,dAD ;}wjz*+8+A]чB{1Gr;4Gx͔`DYڈ"vn;Q k3ϯx5~n V i|lOEaZ߳qa^oSF@pgs2:*k@p k2pss TD"A"ꊫ+...8ՄΟz2 E_9@p r41A h@p['oDC@ w*@ @ @ ^sOoy())=,\1vp8.8g8l6猽q(@ ^iBXXa:(@ @8e7,A @ @pCQD@ &D#kB8e@ @kAA @ Ac<[TWp2Q2^1EMGl*< VHTNoRB([30ٯo8 09dIw_EDt?_ʚmQWd9–[0(3FҥQ˶<]@ C_uH]$_Iz֑Vw A;/ONΫ/crs!YeM叿n"o*6}][$F۳wdž%~Lnhfh:+z pe>s0#a)l""bO'G<dҿ0&xZ:(-31YhPZqk?vO2 O<{' QQ79:$y%o[Yaj 0gTdny0˞B9zΟ3Z;782k`×nñ( # T %U#x4[8GTB _ܭ_My& mcq؋m3#?U @ mu:_/_~ʷ'鐚kXpc*;M#a)˧S7|#4ZHܷY>]FQ@NQ-_CڛxڃTƽ60O[rhy৳XfyP` KPk兕-MRzr֬Tn 2u5˙~}Ifq/Βɴ5i0dmP8ow X9#iWNaE6cA$r > &c&ue̞C#6 JX,#nF"QT.IqeBg͔f{obGwØg>-FU@ >ᶭPh8Ki62e8"t>x薲eSϼpSɔ G0<D=K=1Nfz+\p72-!IҊބB^b?wRC?BSx6b ʐ_ C OI-` e/6$j£F񇈑jr T9:luw9&xezh&yDVniWol$Jq#+w%oT=DpHS&^ fPTuj IAZC`bm?f|WVuK:eA?IȂ^=OGg{ts͓oATazQ^se~=.ikȶ̚4 1 @  psQTW:G}<{JCpBe<0:f½&jSXCkk3Xno|_NW ^BS+Iv{7{{`s /j"Zi^Z~Zv 9 ە4L:_Z1PSm@-2m @%kl.%OYnj&(7ds*njWқ1rIẢZ,[OX;|(:oZy{J/lyQRX`\x8 ^_7 ׷X,Tj/v [zTIyZCt| NF Iv4݃;m̐1ۭe{J6p0? @ @pp۶< Dmc- U{]^ [Ghu>{ i=3x%R^E <¦LI}b.Րe% ֯coX~dx ?2w1۲Jr}:6C!dl7fMa,V;bZG4Bx1B.ŋay'*WCsYdHG H4XXu+O`wYRBN eC4dmXsۚCCyDhX1CQҕv^V`A;+bI1wE9+Vu"Iyֲa+W>p88v;vFkk+h4.d2憛RDD"WWW\\\ەO@p4Rʩ cP14qzzjqqq4gۏs8wF;΍m6PɄa8Aȸ~p; jdtv,vvUPfd~0 IDATTݒ"^)JTFMMԆ3\_f Pfc] *+9[QJLTi5h7 >Zx @ U pZ`@Sð~f*rYc$?K]),2N܏162?NEl lH(&讪۾`ё,EPG2rck2.\()#hJ [zuq9GL9Ƅ<}WTa•L?b,SAƈo<Ҷuz5;j)""){APٶ77sO1/Z*MTh(v/:O<@ W8|-'rLd6hvP]i)/ZM{ٔYhsIϏQXjQ2Ĺ1S!EA{u]S˜ CIe oE._/ >L?iaϱ=<6cLGR =:Ǽ|9O,.P @ ݏ\r7eP4 }(:Tv<?)܆j\+2$f)68|? |CNrP[K> ӆ1B=r4k6{}&fL;Ng;}vo*ĕDA]bܼ݁,]?&]t7qj@Lt<V]g-`%T}-cuv"r5qoR[ YuÊ%nb'F4OF8OD vH rj-eۦ:"F׃5MG3Iy?r3T:$}xLZYYS!+~YFƿ[jZ)ɯ́uoaွ}[RZ3\P"zL;XS͂IDO%o =bཟdͿxqJT[Č/N+w%msƠ8kgJGf8DO#A7V"OrP4EtF*KXBP:nvLmn8iݬFRٝi:^uPz[+8Gmpevnj6> &N,ܧ؛It]yT7lKY 2&`&o~z-_]O+ [A& P漾3בG7Ow_҉7ɿ<6me֓y31eRN37̘KgIIbT"r=3~ӛ0U1|r}be ymƵ7nZF$nLTD/\M4!C5d^rJ7Ŧ),b4}X&^I ),jBYN&" Aby1&՗SGʰ>QIY%(kme %`[m>~IeD' i"w4):`cZg`qeb*~k4 o3C3bs7*FJ?7DwީH|Cz%Z/u+ o_>˔MnMfg3|\.}0%_䳼j'`I"&Vb׵Mb=|ylX Y!V܇E1s=_HΡ&t31|hH FTְ,+f_ѵbN2 kXͫX[Nm'3o~(WgL1i 9[h}P8/#ZYȊRiEcaB_%EBBZƾ: Sވ'/pH^ZdxώFӚq/Ńg4IC;85RX\z\4zY{D 6SPGOʖ4 ¢%=Z0~LnhfhZud0fY$ԲbqvXy|(9:$y%БwYD).VT彂 ֘1z{'gFǰX) 5YOղsdTb/i\v3Xsd$/N-:) N5CTٰ!' : 3ٱ63u,~|4KU:GS)*~zd()9SL͏%@ >SʴxOu.5Q̘!aPz7*66WTj&Jtdڈh?R?\eaJRl&C4)/~!苲ټ(J y9z17ӪAA 1׏xݫ-_6Ky:Rtt+sA|{֊,>KW:%<6 Y&0R56p.dk5i"lUFjQ1yѓύ=|mV`Ll /M&%3a6Q:%6YO1E@ e0/i]_{]NpQ2d ZrXTՁ|\&_oXB,J6k(]rڻ,Lfǩ#X2bчSTW|H>Ko"c\s2zP͜y,\Ȣ}Mњò Q<[d) em1\(? V{0ɦr,T%m̧" u4(g03Z605kJ]'m"TYZ@ &-  gx+ G@9I)i9.hps~Lft4.&TU5@%kFt,cE[XPvkF~3&TT{̐1 sS:)İg&/B^UN/HYaha7;)M$Qo8cEIa@ 70 0(tlkkpp8 ^S9-7xBK/ cgػlXssyмi3x%R^E Gz/<-m]ePT`8Z=k6d^Os5-+z,*&ޘBD.l⍷>X9_9A=qLJ(xl2hzi'9hj\Ú="C/ &~t \?%)F}{[[^ь0(7(\ o 5*44R#&e dW^R|t\;R-,\arޠ}Rl:4﬈G@pGa.N<w4LT*E" Hpuu\\jBO@p\\]ÎsX[>@ho5)$fd< cµ85uNA䝡䦓o gޓwwJTFMMԆ3˜ I8)J~=a@PpV(X8VOMG-:P  V(A@Pw;}^Ϗ9`ǥ~S2r@LEdaay31y|dobD(K>1([TWp2Q|H]$ށ 7ڠچZa° =&ܣ ~kOH.]jg2GN]Gc(;hT}\ *wAV䃣+KHXfԀO)e ~9%L]XL-U(:^Ɇ:~6` cc8A[[&܋ww|;ל / q1@ .NHd RFnAPr)R2)R)ڃ#X]@TބD-zG } 'vsFaDIeI7n^(wwצ[~Yľ췧!$r^㵜H-IH͑Ngxu:̣"a:7gAkNna+X8.3V^TsNdޖ }hx/w}7 ܿeRaÇ߫t.Y|wlupTsh;UWlvBWt?_ʚd$ݝ"5m=wf6[o-]<,ۏǥ87̔|K#]"6J"*m4bپ N$fT/)U@\ Vбƒ@pڮ/A."u:?žVQEgt"%F8uDFT==mli@eÌuM<  <* 1KxGz ͽ}v~ l7oXn ]U@]wArV?vXK,B7_R,uuHIY(۶,goSR rx&6KV/ť劁/-_+(xO}.m*RƗy+kg[ȈTz9V1\˛q-~A7>2U( VL .e&ɡ t(h%-x\8D^E a_ ,[xU}5g>`gQ2Xn%6 <@DHTe+!` 2 ?I"n+DHDJZz)'{Z{2rФpEn~)=KNJ,yhq#\C19sGu]_᭫Cm0pmçLY XXzȟ@B(poZf0Q*{ #x?نeh-0 IDAT8?!p+R0-1r*v^?N#zGZIO 4ApLJdOs{Qt-5qԒ<yJ”Jx jrr$ hI>] r'Oɝt=kAxDB9[lt{0W-!I,mDd?CѱZ=YˆI_ٷ2x&YIڞOvhS D٥\,*cBxPN~ h$u*~WNQX`d$~%*JveQ?ZpLp|F87l }=Hg aJ>S605?^7D{qk#li/lRR"z|b!eET@p qKYXPϸq>nqpjU`Pj(~};z줂ώ ` )t@]l/s^~HMQ PB9y% U2 w]ὶPwŁo".4ghVmmyZ1_Uޭ5;7Ų+x)tGɥ,]\N,D|i~>+ouݸD^+, {ՀϚI?/Lؽr2r\;_{s4@VPp"3ya-Y(NWBKyD4uduz &GzACu'R0 Ԗƃ8&4Ơiqْ0! ya=I$ͷa,P23ŧ*- fH{x1))v3KL(.M?w՟F6N7;2hH;یFMOg/pk5MOG8$KU6L6ח㘘BF=UqRdH iN s]kୢҰ1]DQSe,ߟKx2ǂ(>jq04!ZAml Ge=H9pb ­=lTz?u 0J 54/fŗ Vl3jUjK#tC欮m}iD"sRt84Uvj;C=V,ÈJM#֗8ȿ0 [ZiR&i#m&sQ@lS5\4l>1ܔݟNm1I6OuXl<`A7v BL 6Ew fJ> &ayf[n CL* &'rpb{؆ԙ~/[|i$#JYQXp.1η(Ml1Mì a$aډfk9i߃lg >5YDM_70H&@iGQYwڈ7%o.psiߨT=>'%Bw ^U?)dS&Z6\̈́^hvhnQ9Boz6k]eZ,x TU> oG PKd,=f7֝E3178 c k#aI>Gt gZ9]ž2 I(]1Zg= K(@iJDp2U K*$lІ䭡do խVw#6'l+h&1oU d9|RLSh$Ɔ3eЗ>v."v}m-X.wmz <_9FGCXP'm=h|:K"@[~hu z A+Z(i뻝Gt<2ZD˃";9f=M #L/m7Y}96(&6Rw'%{3 UQ!>|6gg iG'PFA o}{7@paÆ]apLf>t>-&8tO06ixn6_3xbC)^`mN^e:Ҋr*lJ*Zu&u_X^ hw}!wU?fam;44:h쁠^S n{3y3xnr"?YЇf"ɧp^Ď5,x2 cN0`h,h3QM!'"r"sKd}/.0 I Vy PCQL=A5Riphqiu9 qNYIT7۟PFXLìxcL0s30O9tkͭż r'+'Q>g4FjD$Ոnr?Ӑ̊cggMWX`I٩p}m9b5qN6%p$iL.0(f勳xCh^|q!I_˵mt f|n':\ȞZ\ 9{Nbalk=MK gI\.2Cbr0Y oTmĭ&eQxkyݝbA9f)go:}=0~6 [WP8 N2yAWw;5@9J)vDqX8DjuB)t@[S^'&xQwx1Ph股PiF4S&,K[NdXghhnv''O63b*"o.\3Wbҏp{uh:ꩭ&,VLyLg~"q1ճ{E6Ϻc^PQ1{i|xSGjhh3VNQ 'a8Cocڑ3HfbL+1[_̎x|UcI\ec,> Kov';G:ς~f=쬓RpS>ܜu5x'!̛g:QDegv uvwrOJP?fu.]pi69ބ8˾]Y5?~f-dsĭǹ7{gwc#)6 vҀ̉j3֙5ʢ|xXf;҈| GѠSI=p_*!Q[.L|KfVqӘ7+=IdNU_Hp"ϛp;qEKC紓Iū'z&?f{/)=d:A3 sH~ dLvVjJT-ٷ2xUߟ.Lrrwf2Z#@,֕` < MyZ$I ,7cy]Y<+&oT8Yvgqx\ gL-#W6:dی\O/ySjs) D$` `d#/; V2ryG0k\"3`p߳ "w;G!kY^CVYs1}=Hg@  ]:ƒ1mF\xk8y>ʿm&)"};"(c7s1v*/_>1xԎ]U ":Kz {[$~n&b-* RX#վɿ"Ncջ9ueD$oD!Y '%σWG?r~_BC2!D@ (++ o5111A|5dwQ/ a432U |{xU;::x"/^EEQ0 b%@ ( 4^E_3z%&tfp3p^x:::hoo$$& M @ €/~BW@ @ n= ńޖ>\t_aDMTm'۱7 u) }kxfQ@ B7aÖ_wۼ\IJ r`Ho}XA鶵un=v~U#a~-aL BDn-Amw۹@ n<[wABPn~qb/B_B=N&&pF[P蚍' i;n;g?r8y(ҨIFMB2(1@ FP{bttw0!n]tzn6b_yKsB N2f'[GP}C{~aHv5roWww{ #D @pBY ]B'q2 ܼCY 'tmvB3$?$! BPnmA{ .1b߻Ć{hi-Q+QDn~nB_O- ܂~D"B`-a~@pk ݗի}6nЕGɟr`^:qگg^>)lF\:eX|2:z%ۿ^«o2]phã*Pٗ=S V=hB^ͶDx=ٶȌU4eE&%1W%6-eCh?á7]ZGs*?9&n-jzc"{C~aןAQv.@yNIea ݈gZz6l.ڹu-}BPnQz;\֥>2ljt 9{ @p tדk]0f![`x]7[[h>^J[yt<Ij`ɃAmZO^G4mD(Ϸ+'uϑ@?HlnN?.(TEl兤nL'<<D0Ǒ3Gx+8rhԣW W6xф~S#g % L uA y>:s2"JI䧩zVi&N[m "{/t;7Th ]ΌFC,o!v;&l`Yn-jW!Q}fvv\XA.U X8 N,I%,֤{+BPBB{.c׮$&%('*ЋOH"@{ \h{ր9%Zh{;2^% #2{0t)O+XEQWp6VI': I*fPk]ߚGuc#&[YesuA&,&B;L^AaM0gXZ}[Ed]AG!f ( e{)l %gXDOڧw}34>Ki#= 6WW^#JFeя8A͙mdTNKfhʟ_K Wɒ} hBHjS|r[\R1iWX|3<݆j\$)6q}yeQu/Dp/H`ŋ|ˣ4mnnXlίMEdh: ezu/Aٺ&{=+պbL9]ktHm5k#^ :O=M\`YK_+>>G|uP8YNiFVIU޳ݕ1;[fWF(FD'"F܂,nԱZHJ'tgWAv@Q YHyȏ62H`hb~*}ӗ륝irx9ǢY4{wQޏ'`a3Xh.Ąwrp sI)ф6KV/ť]W{m]lN*H<يmnj7Gv_Z1$pE w3(2dFJަUFYr9ŰYK^/ǃw>S?|a{=J>q/[e 0˧t.^odT2G;P8=uK8^Aj.d׏~cx g+[ظ-ckMlbv{ qbȮ~'f&&>| x~6I:*i-g`l`:Mud %<.9"|ǪO-4Lƚ 5M@Sth&tl147CW;Tn`YkX؆ msBG?kaO %nj*kN'JÁKe[(sfE3v,]TA;nW7/Ȩxώ 4E锝qw>DIGP}Di pJO(cl ;>^%r\mj`00D5㡢o`4Vb̡JLE`\ӢI!yz rܭgLxo:RV =4Q_/a|>1bT۩~SF E8V{=aZf`/&54yZ0Hn(yѱW-U1M 8'lN#z\ADfc?ތI\&UT+3EPeyH$}EaFş~o^7#ڭm%XVoKݔ)}[tx$%~ک ^] V.rNxkYg1O1c2}Ɔ&lV<^76^58mE>!̩CkN) łx:C ]Ozv9_#6#N<6YRWο~ u]Pn-,>%(ED> {jE䌂!OMok3AZu驑 |c@ ~ʙ4JēmȦ2l[זR7"mϭv>&_@*/3w9;FiBM個+W+Qx4Xs#'=XLye%]˾ODtS=A6v lWGjVT*_9ޗ@QMp׭#!*(> +9!dm;44꽞\^ooWZ2KVpbVF㨮-.eݚ+$;**Vg r=Sн]IST4irN WP8W_sǔ^WLNaф5( %8:ޫwk 犩 ͬ(͌l"ɧp^Ď5,x2nq[i.jKg(Bl*Z=`==DW-I}؂^}?ƛ194^sȠߋ<! ny"Mlbm\p|/AN:UX lر ;=SYHqm0YYDy#57[ Uvnf[GQMOD/m o!Tә ڝ<ٌR.8rp\ݝU`zv@_yr4;Ł*>(L}9 ;Z##2N/4AD`i(gOYz9@gVm= 8ۀ#.*0Q RpTw2%HU兇8sAaւϿ /!:6-6o)me&yio t u[58Sխ{rMFj}*aG3oyMᯞR.MLE;ybe=qSU\F:𻭤SWPE/vޫT9]^;hY&6~~M//$=/fL"KwχziwAD/{l'$Y[KQ׵ *9͋rZM@fo(0.Wg9d_0ϱ>_tSvJVXܧ}CC^ CnC^C՞|]W#UVc{_yXr^b?²Adae%ď^4`RҳYĊ 7̀m6f扽0Hƨ ZSmz4D<36HyJ/@и J"DKѓIt⫶֚JAlYtxc3+W4!%O/!Z[FY'D0E#sdNNdقh|g8"axUHyǦYs1 ?M@*&Sk]<^ys{=9J/D``MUPU`]sڿleƫ1'qeDžX a$NXCq؞Ou(|ՂâǚUD,XȄi_l2͡biqzC )JH(8c|w–)2"֑E6V*OHhLq؛FtD;8<2Q:_V,GrزX#tGʫhMZXO!<‘d @A-wMX3o~,GKo'y˃Թۀۈ/6s:4jcfƔVC99Etw';gMnyC:Z MOgM~ckʢya2{47Pl23҉++;Ul|rjVs6^Y2W|ތqo4cv.յC*L@ЉIs[C3ɴ6,hZ;CA +4w_gBTC" pІEۻ ôEZ`Ԋד %qf7¿H]Dlfv vU+ZUl?zhbB%x,> "[WLԡkw֖XLb@0Ԩm| !u@Hlʵ2"&0o7|BCvCEM @0 !nDg3F2uHOv}Z _@ L(@F5Ĉ$e*@ |AA }'F N(L@  @pstF FEu9*x@ EuH$^LzF^^|v i+Լ2j("3 Rt'ߩyUϧC/]ռaWUN a@M#"70fh{-wK zf): RR)zg lXk>wi4R"D]9 npL|Ax\ޟCQmo2߫)BzhkaWvr+ 4+bSH.,+l[r$y<݀8LB;ؕ<"YV Uq~O?Pv؇8 <׮<OM+(3n(ԥE(BPPlyylg?C9@9qQ:7ꐩ<+A.$+ KE֐Z>#sWNvX:K Rg?CѱZ=YˆIP[E 6 a=9{df4eEd:piȪif 1A9UB5(a$Z1/{2၉VRcÐ<- <u(4Vs21ذhi9[Ѩ=xFƐL[ȍNohGlXjSBZLLj >˖q/F8?_5P撑; L!eI66gj؏\GFtx[Eɧ:?)]L6݈|"5X-HWQqG'n .$3O_džd.f61q1K' zhEd%%J-ָLL#\;f ONI8e'gE ̳0(WNwSx„5|ԏ9L?c?Bf33C~Z[eb/&|ba3 s6C[ի]߻NJk&>ϴat+I\]yxOS7- GC)l6`bJLR31݈Z /$bcppjn~Y8*ǧ4Q} o0I%Mc\i>^O.8̴hFiƁ,)@ @!`닖&<C;JәN۩2=ngRKbFGM~Mv)?mkb#{wR*ag.Pp< .H1э9T2(@f$$3\-!I@p>r F7JD[_.Ȫh$fb5cbH ISw DЀiOsuX>M0: @ri4Oe g8 ƾ;}U&l=TP:@ <U i0HR0;f6'l+h&1ov?1|Fi CS _5PHi`"4uFil8S  .tp9pCu8a,H$7XU4u1ť.4B;Xjj>r0}LN4sZ8с5mxuѤ,Jo 9o㘚V+

[JX,PW2C'#2*gZ)b&]$@a&0MV(ڗKA8J8oA'16YOH̼8~Jf^ۉ+WI2C'N{.Gk1 8.+# ,C𠌜K>.l [L(ۑb!,[T"2M0v6zF ctK*B/ؓu6v/P+S]]ԊͻFOaCA,(1-#1#:EM6IEP#*F(tF(5BADNF(Ȝ)P9S """""""s@ADDDDDDDL̙CO3wa$^V0͏6: Ϣa3-h~C4?^K3DgzVw${GlSy9Pt+ 篂>vwJ۝Cwfcky bo4 Â*y `,*;|][~®#.ruwSSxy(<*7;SEo; ۺHfe,mN}ւu s0JwCEU NtCIeW h֬2o)Iw  zr|ݼ,qI<9O4t ;+BK 0ǶǾg ? o$IF!wώϮY&p-;`UK~peUXB0Xoѱ1!ye,KufULmA*Vb Nm  MƦz?dꦫOUu5Π%cjulqc UjvZHMUT'3;%|*Jo,WG<'ɃS^b3>=ęGӃbȍ94- Ru[5Fؗ Da[Tݼ\2\~o"8MZ^A.WQu[oˇL7TSUUŲ=t~z?Y|.#uGU0H:'];:Hdx)]I"'DnRs=~lN _<+1p__Nq`Ez<ǔorC"" _POlFLd]l4ljsW_+>/ ,C{g5dc65n?lehi:czKamS-[}kt09˨sXe3 ȃ-$1l&V9rGEؘ ēsp/~ {Ac:d= ճxݩ(m;z+q(r|fw;|z`¢{gJ63y% D̏j/pSz1zyD=6h|+ %8ɹVjG`S4&$QO,9 $I׌Nr"RklK›I~78ǎ9\0(Jc7;)y9{ܔWMVLhKZ};g W#&:<-;h,/eRYBZzæءSNZ.J^Xod kaΫ)p ""g GwpI rr#VXa$>?Nȇd*J.qb gH[8.< Ov( !j.‰"׳wGZ:); 3d窻'ovc\eV5P;AOzT7m/v1q0NmNbuGeimAW8gxx`;zY _ə BaPt86%P2{ ?}RiXgMOu75dXu Ɏ}qOqlYkp˴ 3dh҅6zvId:\DDY–{gm6J’R60iǎ9l)ZZLғCʷhZ^"iPMI׵%YGmGq7.OPsO?BA)wi)ܥւyW`x)-D!W{1m[kiYPPFz~#e~#2D:: isX2<t}(x;]e6Q[¹zJkK >C4.3aȜOtO 8TGݯݔTtme08 j5d\8c@{a!: &Kix~ `^=>jb|!Sexnd{ /Ä{|kݖ6'E\VHr|} } Wm&XN֍Ƴ)~Q!x} 6'-Z5ϱc:M¯uNsDD>'q^g8XMt]]Nъ2 C|k+GKdE鷜'ys<6ӝ¶T,N~KhriEMR\\*(ρBL${a6-KJp-r|0P&""""rS4mm0tiB styd6>8Udjl?]tjvБ![$$Eq7o{09kMQ&}OP1ULO~'Um?o5,?`[02\ElYɀO|;̇gxӨGh˫_vc~X'Q~(IWfZo1ПCwk(R ".y/opӕ7?;%9W`+lg GS[FOu>Ec#I:;e* g|Ic]5 _y~_=~c2~p?y?zk{ "rڹoDZb*ڷlVd⫼XnbKDD#vnW]y“,^tExsK|OՏy 5]%W}lG(|gf]쉟Dz;avXk[ ln:Mᅧv*FsO [Fp؋<Ǒ0QRyS' a'9<&?hO~u\wZ|eߣ#VwI^ +?~[M(5­)a,,[+vG{/j}ld/ЇFDN!u4İYp2**Iy7P"G^A?D[2]Ӻxif=nG1nHɇo{ҵDioP*"g~6RD>w>>Y͙0EG{8u'ِI}v>anona^{:B瓏=zw 0,c_[};ɮfZ˨F6s+}Mm ;) VQ}=DR{c?LtaiF= lnOp=,6ɒzxr+ϒk\p+_u,{6QGIZeK&43XrE!?%^MPxv5yPtMa=a+dٗ/ _Z/ijfl}:6~m MqC6'H,pU2[ɓ-cåok:0 B.Xuԫ8Veҝ|k݅SbxƍRwzC* ."=0}\gGDN;{ If) p,Sl"Ob19`s1'B>rQ\)"gF((G?%*XT@?20 Zp/3I^{|H̾zH"tGqfw/7~RfV^=  /.$#Η{7-[9]XÿLD2\x Py}.}FKobm#79=mѻ?"r+2i kwެgKo ׺u6Ҹ+ XeTl(9U:XGo RR$m" LTt :6i:O/;O^LY11Ϊ6<&vu!PYz=mn#7++@>5'hvqO eA?i-u8Jw2&ƥW(m Mt%yOIEDDDȬg瓟74#1dbb_IKCNݫ*pOݽ^`{pwk;hqwUVbabCC&.NQ΃wA7''i7Rךm{YZ]ky+(:~`dl+pG}]$ +^(FM.6IE1RLqQExgv3SA/uʯ/6vt{(t-W\YkI J^7tgb.~u}9ټŀ#&%aS͸.Ýqޣ͉rmMsNknXykI"ʗ;ad7r<4Sczr'Irfp:;Ļe=`$VbNwa` ;)Zg$B-4f`bțY]5 &Kix~ `^=77;9B&-;[!Mn ,)`-;dC7,<+촼t$!Eqp# J ,{1R͟g[ >\nP߸B`,P}稙G^sLVr4 i)*QcEDD j@DnFoۀHK*B9%^PM7T"rvͻFOa](id2fD(rnDDDN&W*LE(""r O A4BADN u9(PSI{y\DD<) pQϟ#|[ży{]DN? """""g?6gg D%"r6*3#qafH:i@ADDDD,>.LoG*$9(eMM#GTH"r)P"""r͟?}3~Av DTiERRHRA'_DdJ DdLl!""r,rY8P!Jqyz>^\DfE?)"s52_5@DDDΰyQ{yѣ)ϷEYs:Ȍ(d3`޼y8gƟç tckcRuR "@ADfkɚƇ DDD>cFjdLJLga|pttqc(Px!c]ƾx 9)P4EFXCR񉈈u,QDSF' ƈBJ񡂂"2fxjƈ] Ä.} "y*E0oܿq]eb[c'32ߦD]& G9$(ȧtɃL7m5068M!2U0YhADDԄ # Ӆ /wQ "2q2} qL}1)Bd?9r "q&P`@a22F' &Rw ]0AD( I+6>6E@ADD L*L fs9x)Ca &d5Nv{."rQ$j"""rzxCjAaH#Dd. yhT{dAB3x{A?)"l1 DDDx @ad@DDɂ] ""2e3=FDdNzGDD#sxU"r|& wDDNU]zFDDe&">(LuDDủQ_DTȬ)HYH2%&$ТIENDB`icingadb-web-1.4.0/library/000077500000000000000000000000001516124654200155015ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/000077500000000000000000000000001516124654200172015ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Authentication/000077500000000000000000000000001516124654200221605ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Authentication/ObjectAuthorization.php000066400000000000000000000212101516124654200266540ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Authentication; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use InvalidArgumentException; use ipl\Orm\Compat\FilterProcessor; use ipl\Orm\Model; use ipl\Sql\Expression; use ipl\Sql\Select; use ipl\Stdlib\Filter; class ObjectAuthorization { use Auth; use Database; /** @var array */ protected static $knownGrants = []; /** * Caches already applied filters to an object * * @var array */ protected static $matchedFilters = []; /** * Check whether the permission is granted on the object * * @param string $permission * @param Model $for The object * * @return bool */ public static function grantsOn(string $permission, Model $for): bool { $self = new static(); $tableName = $for->getTableName(); $uniqueId = $for->{$for->getKeyName()}; if (! isset($uniqueId)) { return false; } if (! isset(self::$knownGrants[$tableName][$uniqueId])) { $self->loadGrants( get_class($for), Filter::equal($for->getKeyName(), $uniqueId), $uniqueId, false ); } return $self->checkGrants($permission, self::$knownGrants[$tableName][$uniqueId]); } /** * Check whether the permission is granted on objects matching the type and filter * * The check will be performed on every object matching the filter. Though the result * only allows to determine whether the permission is granted on **any** or *none* * of the objects in question. Any subsequent call to {@see ObjectAuthorization::grantsOn} * will make use of the underlying results the check has determined in order to avoid * unnecessary queries. * * @param string $permission * @param string $type * @param Filter\Rule $filter * @param bool $cache Pass `false` to not perform the check on every object * * @return bool */ public static function grantsOnType(string $permission, string $type, Filter\Rule $filter, bool $cache = true): bool { switch ($type) { case 'host': $for = Host::class; break; case 'service': $for = Service::class; break; case 'dependency_node': $for = DependencyNode::class; break; default: throw new InvalidArgumentException(sprintf('Unknown type "%s"', $type)); } $self = new static(); $uniqueId = spl_object_hash($filter); if (! isset(self::$knownGrants[$type][$uniqueId])) { $self->loadGrants($for, $filter, $uniqueId, $cache); } return $self->checkGrants($permission, self::$knownGrants[$type][$uniqueId]); } /** * Check whether the given filter matches on the given object * * @param string $queryString * @param Model $object * * @return bool */ public static function matchesOn(string $queryString, Model $object): bool { $self = new static(); $uniqueId = $object->{$object->getKeyName()}; if (! isset(self::$matchedFilters[$queryString][$uniqueId])) { $restriction = 'icingadb/filter/services'; if ($object instanceof Host) { $restriction = 'icingadb/filter/hosts'; } $filter = $self->parseRestriction($queryString, $restriction); $query = $object::on($self->getDb()); $query ->filter($filter) ->filter(Filter::equal($object->getKeyName(), $uniqueId)) ->columns([new Expression('1')]); $result = $query->execute()->hasResult(); self::$matchedFilters[$queryString][$uniqueId] = $result; return $result; } return self::$matchedFilters[$queryString][$uniqueId]; } /** * Load all the user's roles that grant access to at least one object matching the filter * * @param string $model The class path to the object model * @param Filter\Rule $filter * @param string $cacheKey * @param bool $cache Pass `false` to not populate the cache with the matching objects * * @return void */ protected function loadGrants(string $model, Filter\Rule $filter, string $cacheKey, bool $cache = true) { /** @var Model $model */ $query = $model::on($this->getDb()); $tableName = $query->getModel()->getTableName(); $inspectedRoles = []; $roleExpressions = []; $rolesWithoutRestrictions = []; foreach ($this->getAuth()->getUser()->getRoles() as $role) { $roleFilter = Filter::all(); if (($restriction = $role->getRestrictions('icingadb/filter/objects'))) { $roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/objects')); } if ($tableName === 'host' || $tableName === 'service' || $tableName === 'dependency_node') { if (($restriction = $role->getRestrictions('icingadb/filter/hosts'))) { $roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/hosts')); } } if ( ($tableName === 'dependency_node' || $tableName === 'service') && ($restriction = $role->getRestrictions('icingadb/filter/services')) ) { $roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/services')); } if ($roleFilter->isEmpty()) { $rolesWithoutRestrictions[] = $role->getName(); continue; } $roleName = str_replace('.', '_', $role->getName()); $inspectedRoles[$roleName] = $role->getName(); $roleName = $this->getDb()->quoteIdentifier($roleName); if ($cache) { FilterProcessor::apply($roleFilter, $query); $where = $query->getSelectBase()->getWhere(); $query->getSelectBase()->resetWhere(); $values = []; $rendered = $this->getDb()->getQueryBuilder()->buildCondition($where, $values); $roleExpressions[$roleName] = new Expression($rendered, null, ...$values); } else { $subQuery = clone $query; $roleExpressions[$roleName] = $subQuery ->columns([new Expression('1')]) ->filter($roleFilter) ->filter($filter) ->limit(1) ->assembleSelect() ->resetOrderBy(); } } $rolesWithRestrictions = []; if (! empty($roleExpressions)) { if ($cache) { $query->columns('id')->withColumns($roleExpressions); $query->filter($filter); } else { $query = [$this->getDb()->fetchOne((new Select())->columns($roleExpressions))]; } foreach ($query as $row) { $roles = $rolesWithoutRestrictions; foreach ($inspectedRoles as $alias => $roleName) { if ($row->$alias) { $rolesWithRestrictions[$roleName] = true; $roles[] = $roleName; } } if ($cache) { self::$knownGrants[$tableName][$row->id] = $roles; } } } self::$knownGrants[$tableName][$cacheKey] = array_merge( $rolesWithoutRestrictions, array_keys($rolesWithRestrictions) ); } /** * Check if any of the given roles grants the permission * * @param string $permission * @param array $roles * * @return bool */ protected function checkGrants(string $permission, array $roles): bool { if (empty($roles)) { return false; } $granted = false; foreach ($this->getAuth()->getUser()->getRoles() as $role) { if ($role->denies($permission)) { return false; } elseif ($granted || ! $role->grants($permission)) { continue; } $granted = in_array($role->getName(), $roles, true); } return $granted; } } icingadb-web-1.4.0/library/Icingadb/Command/000077500000000000000000000000001516124654200205575ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Command/IcingaApiCommand.php000066400000000000000000000046041516124654200244170ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command; class IcingaApiCommand { /** * Command data * * @var array */ protected $data; /** * Name of the endpoint * * @var string */ protected $endpoint; /** * HTTP method to use * * @var string */ protected $method = 'POST'; /** * Create a new Icinga 2 API command * * @param string $endpoint * @param array $data * * @return static */ public static function create(string $endpoint, array $data): self { return (new static()) ->setEndpoint($endpoint) ->setData($data); } /** * Get the command data * * @return array */ public function getData(): array { if ($this->data === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->data; } /** * Set the command data * * @param array $data * * @return $this */ public function setData(array $data): self { $this->data = $data; return $this; } /** * Get the name of the endpoint * * @return string */ public function getEndpoint(): string { if ($this->endpoint === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->endpoint; } /** * Set the name of the endpoint * * @param string $endpoint * * @return $this */ public function setEndpoint(string $endpoint): self { $this->endpoint = $endpoint; return $this; } /** * Get the HTTP method to use * * @return string */ public function getMethod(): string { return $this->method; } /** * Set the HTTP method to use * * @param string $method All uppercase HTTP method name. Case-sensitive. * * @return $this */ public function setMethod(string $method): self { $this->method = $method; return $this; } } icingadb-web-1.4.0/library/Icingadb/Command/IcingaCommand.php000066400000000000000000000010121516124654200237530ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command; /** * Base class for commands sent to an Icinga instance */ abstract class IcingaCommand { /** * Get the name of the command * * @return string */ public function getName(): string { $nsParts = explode('\\', get_called_class()); return substr_replace(end($nsParts), '', -7); // Remove 'Command' Suffix } } icingadb-web-1.4.0/library/Icingadb/Command/Instance/000077500000000000000000000000001516124654200223235ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Command/Instance/ToggleInstanceFeatureCommand.php000066400000000000000000000052551516124654200305640ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Instance; use Icinga\Module\Icingadb\Command\IcingaCommand; /** * Enable or disable a feature of an Icinga instance */ class ToggleInstanceFeatureCommand extends IcingaCommand { /** * Feature for enabling or disabling active host checks on an Icinga instance */ public const FEATURE_ACTIVE_HOST_CHECKS = 'active_host_checks_enabled'; /** * Feature for enabling or disabling active service checks on an Icinga instance */ public const FEATURE_ACTIVE_SERVICE_CHECKS = 'active_service_checks_enabled'; /** * Feature for enabling or disabling host and service event handlers on an Icinga instance */ public const FEATURE_EVENT_HANDLERS = 'event_handlers_enabled'; /** * Feature for enabling or disabling host and service flap detection on an Icinga instance */ public const FEATURE_FLAP_DETECTION = 'flap_detection_enabled'; /** * Feature for enabling or disabling host and service notifications on an Icinga instance */ public const FEATURE_NOTIFICATIONS = 'notifications_enabled'; /** * Feature for enabling or disabling the processing of host and service performance data on an Icinga instance */ public const FEATURE_PERFORMANCE_DATA = 'process_performance_data'; /** * Feature that is to be enabled or disabled * * @var string */ protected $feature; /** * Whether the feature should be enabled or disabled * * @var bool */ protected $enabled; /** * Set the feature that is to be enabled or disabled * * @param string $feature * * @return $this */ public function setFeature(string $feature): self { $this->feature = $feature; return $this; } /** * Get the feature that is to be enabled or disabled * * @return string */ public function getFeature(): string { if ($this->feature === null) { throw new \LogicException('You have to set the feature first before getting it.'); } return $this->feature; } /** * Set whether the feature should be enabled or disabled * * @param bool $enabled * * @return $this */ public function setEnabled(bool $enabled = true): self { $this->enabled = $enabled; return $this; } /** * Get whether the feature should be enabled or disabled * * @return ?bool */ public function getEnabled() { return $this->enabled; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/000077500000000000000000000000001516124654200217655ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Command/Object/AcknowledgeProblemCommand.php000066400000000000000000000055771516124654200275570ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Acknowledge a host or service problem */ class AcknowledgeProblemCommand extends WithCommentCommand { /** * Whether the acknowledgement is sticky * * Sticky acknowledgements remain until the host or service recovers. Non-sticky acknowledgements will be * automatically removed when the host or service state changes. * * @var bool */ protected $sticky = false; /** * Whether to send a notification about the acknowledgement * @var bool */ protected $notify = false; /** * Whether the comment associated with the acknowledgement is persistent * * Persistent comments are not lost the next time the monitoring host restarts. * * @var bool */ protected $persistent = false; /** * Optional time when the acknowledgement should expire * * @var int */ protected $expireTime; /** * Set whether the acknowledgement is sticky * * @param bool $sticky * * @return $this */ public function setSticky(bool $sticky = true): self { $this->sticky = $sticky; return $this; } /** * Is the acknowledgement sticky? * * @return bool */ public function getSticky(): bool { return $this->sticky; } /** * Set whether to send a notification about the acknowledgement * * @param bool $notify * * @return $this */ public function setNotify(bool $notify = true): self { $this->notify = $notify; return $this; } /** * Get whether to send a notification about the acknowledgement * * @return bool */ public function getNotify(): bool { return $this->notify; } /** * Set whether the comment associated with the acknowledgement is persistent * * @param bool $persistent * * @return $this */ public function setPersistent(bool $persistent = true): self { $this->persistent = $persistent; return $this; } /** * Is the comment associated with the acknowledgement is persistent? * * @return bool */ public function getPersistent(): bool { return $this->persistent; } /** * Set the time when the acknowledgement should expire * * @param int $expireTime * * @return $this */ public function setExpireTime(int $expireTime): self { $this->expireTime = $expireTime; return $this; } /** * Get the time when the acknowledgement should expire * * @return ?int */ public function getExpireTime() { return $this->expireTime; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/AddCommentCommand.php000066400000000000000000000015311516124654200260100ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Add a comment to a host or service */ class AddCommentCommand extends WithCommentCommand { /** * Optional time when the acknowledgement should expire * * @var int */ protected $expireTime; /** * Set the time when the acknowledgement should expire * * @param int $expireTime * * @return $this */ public function setExpireTime(int $expireTime): self { $this->expireTime = $expireTime; return $this; } /** * Get the time when the acknowledgement should expire * * @return ?int */ public function getExpireTime() { return $this->expireTime; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/CommandAuthor.php000066400000000000000000000015331516124654200252410ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; trait CommandAuthor { /** * Author of the command * * @var string */ protected $author; /** * Set the author * * @param string $author * * @return $this */ public function setAuthor(string $author): self { $this->author = $author; return $this; } /** * Get the author * * @return string */ public function getAuthor(): string { if ($this->author === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->author; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/DeleteCommentCommand.php000066400000000000000000000004411516124654200265210ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Delete a host or service comment */ class DeleteCommentCommand extends ObjectsCommand { use CommandAuthor; } icingadb-web-1.4.0/library/Icingadb/Command/Object/DeleteDowntimeCommand.php000066400000000000000000000004431516124654200267070ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Delete a host or service downtime */ class DeleteDowntimeCommand extends ObjectsCommand { use CommandAuthor; } icingadb-web-1.4.0/library/Icingadb/Command/Object/GetObjectCommand.php000066400000000000000000000012541516124654200256450ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; class GetObjectCommand extends ObjectsCommand { /** @var array */ protected $attributes; /** * Get the attributes to query * * @return ?array */ public function getAttributes() { return $this->attributes; } /** * Set the attributes to query * * @param array $attributes * * @return $this */ public function setAttributes(array $attributes): self { $this->attributes = $attributes; return $this; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/ObjectsCommand.php000066400000000000000000000043561516124654200253760ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; use ArrayIterator; use Generator; use Icinga\Module\Icingadb\Command\IcingaCommand; use InvalidArgumentException; use ipl\Orm\Model; use Iterator; use LogicException; use Traversable; /** * Base class for commands that involve monitored objects, i.e. hosts or services */ abstract class ObjectsCommand extends IcingaCommand { /** * Involved objects * * @var ?Iterator */ protected $objects; /** * How many objects to process at once * * @var ?int */ protected ?int $chunkSize = null; /** * Set the involved objects * * @param Iterator $objects Except generators * * @return $this * * @throws InvalidArgumentException If a generator is passed */ public function setObjects(Iterator $objects): self { if ($objects instanceof Generator) { throw new InvalidArgumentException('Generators are not supported'); } $this->objects = $objects; return $this; } /** * Set the involved object * * @param Model $object * * @return $this * * @deprecated Use setObjects() instead */ public function setObject(Model $object): self { return $this->setObjects(new ArrayIterator([$object])); } /** * Get the involved objects * * @return Iterator */ public function getObjects(): Iterator { if ($this->objects === null) { throw new LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->objects; } /** * Set how many objects to process at once * * @param ?int $chunkSize * * @return $this */ public function setChunkSize(?int $chunkSize): static { $this->chunkSize = $chunkSize; return $this; } /** * Get how many objects to process at once * * @return ?int */ public function getChunkSize(): ?int { return $this->chunkSize; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/ProcessCheckResultCommand.php000066400000000000000000000052501516124654200275520ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Submit a passive check result for a host or service */ class ProcessCheckResultCommand extends ObjectsCommand { /** * Host up */ public const HOST_UP = 0; /** * Host down */ public const HOST_DOWN = 1; /** * Service ok */ public const SERVICE_OK = 0; /** * Service warning */ public const SERVICE_WARNING = 1; /** * Service critical */ public const SERVICE_CRITICAL = 2; /** * Service unknown */ public const SERVICE_UNKNOWN = 3; /** * Status code of the host or service check result * * @var int */ protected $status; /** * Text output of the host or service check result * * @var string */ protected $output; /** * Optional performance data of the host or service check result * * @var string */ protected $performanceData; /** * Set the status code of the host or service check result * * @param int $status * * @return $this */ public function setStatus(int $status): self { $this->status = $status; return $this; } /** * Get the status code of the host or service check result * * @return int */ public function getStatus(): int { if ($this->status === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->status; } /** * Set the text output of the host or service check result * * @param string $output * * @return $this */ public function setOutput(string $output): self { $this->output = $output; return $this; } /** * Get the text output of the host or service check result * * @return ?string */ public function getOutput() { return $this->output; } /** * Set the performance data of the host or service check result * * @param ?string $performanceData * * @return $this */ public function setPerformanceData(?string $performanceData = null): self { $this->performanceData = $performanceData; return $this; } /** * Get the performance data of the host or service check result * * @return ?string */ public function getPerformanceData() { return $this->performanceData; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/PropagateHostDowntimeCommand.php000066400000000000000000000021771516124654200302730ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Schedule and propagate host downtime * * @deprecated Use {@see ScheduleDowntimeCommand} instead */ class PropagateHostDowntimeCommand extends ScheduleHostDowntimeCommand { protected $childOption = ScheduleDowntimeCommand::SCHEDULE_CHILDREN; /** * Set whether the downtime for child hosts are all set to be triggered by this' host downtime * * @param bool $triggered * * @return $this */ public function setTriggered(bool $triggered = true): self { return $this->setChildOption( $triggered ? ScheduleDowntimeCommand::TRIGGER_CHILDREN : ScheduleDowntimeCommand::SCHEDULE_CHILDREN ); } /** * Get whether the downtime for child hosts are all set to be triggered by this' host downtime * * @return bool */ public function getTriggered(): bool { return $this->getChildOption() === ScheduleDowntimeCommand::TRIGGER_CHILDREN; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/RemoveAcknowledgementCommand.php000066400000000000000000000005001516124654200302550ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Remove a problem acknowledgement from a host or service */ class RemoveAcknowledgementCommand extends ObjectsCommand { use CommandAuthor; } icingadb-web-1.4.0/library/Icingadb/Command/Object/ScheduleCheckCommand.php000066400000000000000000000042371516124654200264750ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Schedule a check */ class ScheduleCheckCommand extends ObjectsCommand { /** * Time when the next check of a host or service is to be scheduled * * If active checks are disabled on a host- or service-specific or program-wide basis or the host or service is * already scheduled to be checked at an earlier time, etc. The check may not actually be scheduled at the time * specified. This behaviour can be overridden by setting `ScheduledCheck::$forced' to true. * * @var int Unix timestamp */ protected $checkTime; /** * Whether the check is forced * * Forced checks are performed regardless of what time it is (e.g. time period restrictions are ignored) and whether * or not active checks are enabled on a host- or service-specific or program-wide basis. * * @var bool */ protected $forced = false; /** * Set the time when the next check of a host or service is to be scheduled * * @param int $checkTime Unix timestamp * * @return $this */ public function setCheckTime(int $checkTime): self { $this->checkTime = $checkTime; return $this; } /** * Get the time when the next check of a host or service is to be scheduled * * @return int Unix timestamp */ public function getCheckTime(): int { if ($this->checkTime === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->checkTime; } /** * Set whether the check is forced * * @param bool $forced * * @return $this */ public function setForced(bool $forced = true): self { $this->forced = $forced; return $this; } /** * Get whether the check is forced * * @return bool */ public function getForced(): bool { return $this->forced; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/ScheduleDowntimeCommand.php000066400000000000000000000141531516124654200272440ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Schedule a host or service downtime * * @phpstan-type CHILD_OPTION self::IGNORE_CHILDREN|self::TRIGGER_CHILDREN|self::SCHEDULE_CHILDREN */ class ScheduleDowntimeCommand extends AddCommentCommand { /** @var int Do not do anything with children */ public const IGNORE_CHILDREN = 0; /** @var int Schedule a downtime for all children and trigger them by this downtime */ public const TRIGGER_CHILDREN = 1; /** @var int Schedule a downtime for all children but do not trigger them by this downtime */ public const SCHEDULE_CHILDREN = 2; /** * Downtime starts at the exact time specified * * If `Downtime::$fixed' is set to false, the time between `Downtime::$start' and `Downtime::$end' at which a * host or service transitions to a problem state determines the time at which the downtime actually starts. * The downtime will then last for `Downtime::$duration' seconds. * * @var int Unix timestamp */ protected $start; /** * Downtime ends at the exact time specified * * If `Downtime::$fixed' is set to false, the time between `Downtime::$start' and `Downtime::$end' at which a * host or service transitions to a problem state determines the time at which the downtime actually starts. * The downtime will then last for `Downtime::$duration' seconds. * * @var int Unix timestamp */ protected $end; /** * Whether it's a fixed or flexible downtime * * @var bool */ protected $fixed = true; /** * ID of the downtime which triggers this downtime * * The start of this downtime is triggered by the start of the other scheduled host or service downtime. * * @var int|null */ protected $triggerId; /** * The duration in seconds the downtime must last if it's a flexible downtime * * If `Downtime::$fixed' is set to false, the downtime will last for the duration in seconds specified, even * if the host or service recovers before the downtime expires. * * @var int|null */ protected $duration; /** * Whether to schedule a downtime for all services associated with a particular host * * @var bool */ protected $forAllServices = false; /** * What to do with children * * @var CHILD_OPTION */ protected $childOption = self::IGNORE_CHILDREN; /** * Set the time when the downtime should start * * @param int $start Unix timestamp * * @return $this */ public function setStart(int $start): self { $this->start = $start; return $this; } /** * Get the time when the downtime should start * * @return int Unix timestamp */ public function getStart(): int { if ($this->start === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->start; } /** * Set the time when the downtime should end * * @param int $end Unix timestamp * * @return $this */ public function setEnd(int $end): self { $this->end = $end; return $this; } /** * Get the time when the downtime should end * * @return int Unix timestamp */ public function getEnd(): int { if ($this->start === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->end; } /** * Set whether it's a fixed or flexible downtime * * @param boolean $fixed * * @return $this */ public function setFixed(bool $fixed = true): self { $this->fixed = $fixed; return $this; } /** * Is the downtime fixed? * * @return boolean */ public function getFixed(): bool { return $this->fixed; } /** * Set the ID of the downtime which triggers this downtime * * @param int $triggerId * * @return $this */ public function setTriggerId(int $triggerId): self { $this->triggerId = $triggerId; return $this; } /** * Get the ID of the downtime which triggers this downtime * * @return int|null */ public function getTriggerId() { return $this->triggerId; } /** * Set the duration in seconds the downtime must last if it's a flexible downtime * * @param int $duration * * @return $this */ public function setDuration(int $duration): self { $this->duration = $duration; return $this; } /** * Get the duration in seconds the downtime must last if it's a flexible downtime * * @return int|null */ public function getDuration() { return $this->duration; } /** * Set whether to schedule a downtime for all services associated with a particular host * * @param bool $forAllServices * * @return $this */ public function setForAllServices(bool $forAllServices = true): self { $this->forAllServices = $forAllServices; return $this; } /** * Get whether to schedule a downtime for all services associated with a particular host * * @return bool */ public function getForAllServices(): bool { return $this->forAllServices; } /** * Set what to do with children * * @param CHILD_OPTION $option * * @return $this */ public function setChildOption(int $option): self { $this->childOption = $option; return $this; } /** * Get what to do with children * * @return CHILD_OPTION */ public function getChildOption(): int { return $this->childOption; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/ScheduleHostDowntimeCommand.php000066400000000000000000000005261516124654200301010ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Schedule a host downtime * * @deprecated Use {@see ScheduleDowntimeCommand} instead */ class ScheduleHostDowntimeCommand extends ScheduleServiceDowntimeCommand { } icingadb-web-1.4.0/library/Icingadb/Command/Object/ScheduleServiceDowntimeCommand.php000066400000000000000000000006521516124654200305640ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Schedule a service downtime * * @deprecated Use {@see ScheduleDowntimeCommand} instead */ class ScheduleServiceDowntimeCommand extends ScheduleDowntimeCommand { public function getName(): string { return 'ScheduleDowntime'; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/SendCustomNotificationCommand.php000066400000000000000000000016551516124654200304370ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Send custom notifications for a host or service */ class SendCustomNotificationCommand extends WithCommentCommand { /** * Whether the notification is forced * * Forced notifications are sent out regardless of time restrictions and whether or not notifications are enabled. * * @var bool */ protected $forced; /** * Get whether to force the notification * * @return ?bool */ public function getForced() { return $this->forced; } /** * Set whether to force the notification * * @param bool $forced * * @return $this */ public function setForced(bool $forced = true): self { $this->forced = $forced; return $this; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/ToggleObjectFeatureCommand.php000066400000000000000000000051631516124654200276660ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Enable or disable a feature of an Icinga object, i.e. host or service */ class ToggleObjectFeatureCommand extends ObjectsCommand { /** * Feature for enabling or disabling active checks of a host or service */ public const FEATURE_ACTIVE_CHECKS = 'active_checks_enabled'; /** * Feature for enabling or disabling passive checks of a host or service */ public const FEATURE_PASSIVE_CHECKS = 'passive_checks_enabled'; /** * Feature for enabling or disabling notifications for a host or service * * Notifications will be sent out only if notifications are enabled on a program-wide basis as well. */ public const FEATURE_NOTIFICATIONS = 'notifications_enabled'; /** * Feature for enabling or disabling event handler for a host or service */ public const FEATURE_EVENT_HANDLER = 'event_handler_enabled'; /** * Feature for enabling or disabling flap detection for a host or service. * * In order to enable flap detection flap detection must be enabled on a program-wide basis as well. */ public const FEATURE_FLAP_DETECTION = 'flapping_enabled'; /** * Feature that is to be enabled or disabled * * @var string */ protected $feature; /** * Whether the feature should be enabled or disabled * * @var bool */ protected $enabled; /** * Set the feature that is to be enabled or disabled * * @param string $feature * * @return $this */ public function setFeature(string $feature): self { $this->feature = $feature; return $this; } /** * Get the feature that is to be enabled or disabled * * @return string */ public function getFeature(): string { if ($this->feature === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->feature; } /** * Set whether the feature should be enabled or disabled * * @param bool $enabled * * @return $this */ public function setEnabled(bool $enabled = true): self { $this->enabled = $enabled; return $this; } /** * Get whether the feature should be enabled or disabled * * @return ?bool */ public function getEnabled() { return $this->enabled; } } icingadb-web-1.4.0/library/Icingadb/Command/Object/WithCommentCommand.php000066400000000000000000000017101516124654200262320ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Object; /** * Base class for commands adding comments */ abstract class WithCommentCommand extends ObjectsCommand { use CommandAuthor; /** * Comment * * @var string */ protected $comment; /** * Set the comment * * @param string $comment * * @return $this */ public function setComment(string $comment): self { $this->comment = $comment; return $this; } /** * Get the comment * * @return string */ public function getComment(): string { if ($this->comment === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->comment; } } icingadb-web-1.4.0/library/Icingadb/Command/Renderer/000077500000000000000000000000001516124654200223255ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php000066400000000000000000000260661516124654200276620ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Renderer; use Icinga\Module\Icingadb\Command\IcingaApiCommand; use Icinga\Module\Icingadb\Command\Object\GetObjectCommand; use Icinga\Module\Icingadb\Command\Object\ScheduleDowntimeCommand; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Command\Instance\ToggleInstanceFeatureCommand; use Icinga\Module\Icingadb\Command\Object\AcknowledgeProblemCommand; use Icinga\Module\Icingadb\Command\Object\AddCommentCommand; use Icinga\Module\Icingadb\Command\Object\DeleteCommentCommand; use Icinga\Module\Icingadb\Command\Object\DeleteDowntimeCommand; use Icinga\Module\Icingadb\Command\Object\ProcessCheckResultCommand; use Icinga\Module\Icingadb\Command\Object\RemoveAcknowledgementCommand; use Icinga\Module\Icingadb\Command\Object\ScheduleCheckCommand; use Icinga\Module\Icingadb\Command\Object\SendCustomNotificationCommand; use Icinga\Module\Icingadb\Command\Object\ToggleObjectFeatureCommand; use Icinga\Module\Icingadb\Command\IcingaCommand; use InvalidArgumentException; use ipl\Orm\Model; use LogicException; use Traversable; /** * Icinga command renderer for the Icinga command file */ class IcingaApiCommandRenderer implements IcingaCommandRendererInterface { /** * Name of the Icinga application object * * @var string */ protected $app = 'app'; /** * Get the name of the Icinga application object * * @return string */ public function getApp(): string { return $this->app; } /** * Set the name of the Icinga application object * * @param string $app * * @return $this */ public function setApp(string $app): self { $this->app = $app; return $this; } /** * Apply filter to query data * * @param array $data * @param Traversable $objects * * @return ?Model Any of the objects (useful for further type-dependent handling) */ protected function applyFilter(array &$data, Traversable $objects): ?Model { $object = null; foreach ($objects as $object) { if ($object instanceof Service) { $data['services'][] = sprintf('%s!%s', $object->host->name, $object->name); } else { $data['hosts'][] = $object->name; } } return $object; } /** * Get the sub-route of the endpoint for an object * * @param Model $object * * @return string */ protected function getObjectPluralType(Model $object): string { if ($object instanceof Host) { return 'hosts'; } if ($object instanceof Service) { return 'services'; } throw new LogicException(sprintf('Invalid object type %s provided', get_class($object))); } /** * Render a command * * @param IcingaCommand $command * * @return IcingaApiCommand */ public function render(IcingaCommand $command): IcingaApiCommand { $renderMethod = 'render' . $command->getName(); if (! method_exists($this, $renderMethod)) { throw new InvalidArgumentException( sprintf('Can\'t render command. Method %s not found', $renderMethod) ); } return $this->$renderMethod($command); } public function renderGetObject(GetObjectCommand $command): IcingaApiCommand { $data = [ 'all_joins' => 1, 'attrs' => $command->getAttributes() ]; $endpoint = 'objects/' . $this->getObjectPluralType($this->applyFilter($data, $command->getObjects())); return IcingaApiCommand::create($endpoint, $data)->setMethod('GET'); } public function renderAddComment(AddCommentCommand $command): IcingaApiCommand { $endpoint = 'actions/add-comment'; $data = [ 'author' => $command->getAuthor(), 'comment' => $command->getComment() ]; if ($command->getExpireTime() !== null) { $data['expiry'] = $command->getExpireTime(); } $this->applyFilter($data, $command->getObjects()); return IcingaApiCommand::create($endpoint, $data); } public function renderSendCustomNotification(SendCustomNotificationCommand $command): IcingaApiCommand { $endpoint = 'actions/send-custom-notification'; $data = [ 'author' => $command->getAuthor(), 'comment' => $command->getComment(), 'force' => $command->getForced() ]; $this->applyFilter($data, $command->getObjects()); return IcingaApiCommand::create($endpoint, $data); } public function renderProcessCheckResult(ProcessCheckResultCommand $command): IcingaApiCommand { $endpoint = 'actions/process-check-result'; $data = [ 'exit_status' => $command->getStatus(), 'plugin_output' => $command->getOutput(), 'performance_data' => $command->getPerformanceData() ]; $this->applyFilter($data, $command->getObjects()); return IcingaApiCommand::create($endpoint, $data); } public function renderScheduleCheck(ScheduleCheckCommand $command): IcingaApiCommand { $endpoint = 'actions/reschedule-check'; $data = [ 'next_check' => $command->getCheckTime(), 'force' => $command->getForced() ]; $this->applyFilter($data, $command->getObjects()); return IcingaApiCommand::create($endpoint, $data); } public function renderScheduleDowntime(ScheduleDowntimeCommand $command): IcingaApiCommand { $endpoint = 'actions/schedule-downtime'; $data = [ 'author' => $command->getAuthor(), 'comment' => $command->getComment(), 'start_time' => $command->getStart(), 'end_time' => $command->getEnd(), 'duration' => $command->getDuration(), 'fixed' => $command->getFixed(), 'trigger_name' => $command->getTriggerId(), 'child_options' => $command->getChildOption() ]; if ($command->getForAllServices()) { $data['all_services'] = true; } $this->applyFilter($data, $command->getObjects()); return IcingaApiCommand::create($endpoint, $data); } public function renderAcknowledgeProblem(AcknowledgeProblemCommand $command): IcingaApiCommand { $endpoint = 'actions/acknowledge-problem'; $data = [ 'author' => $command->getAuthor(), 'comment' => $command->getComment(), 'sticky' => $command->getSticky(), 'notify' => $command->getNotify(), 'persistent' => $command->getPersistent() ]; if ($command->getExpireTime() !== null) { $data['expiry'] = $command->getExpireTime(); } $this->applyFilter($data, $command->getObjects()); return IcingaApiCommand::create($endpoint, $data); } public function renderToggleObjectFeature(ToggleObjectFeatureCommand $command): IcingaApiCommand { switch ($command->getFeature()) { case ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS: $attr = 'enable_active_checks'; break; case ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS: $attr = 'enable_passive_checks'; break; case ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS: $attr = 'enable_notifications'; break; case ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER: $attr = 'enable_event_handler'; break; case ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION: $attr = 'enable_flapping'; break; default: throw new InvalidArgumentException($command->getFeature()); } $endpoint = 'objects/'; $objects = $command->getObjects(); $data = [ 'attrs' => [ $attr => $command->getEnabled() ] ]; $endpoint .= $this->getObjectPluralType($this->applyFilter($data, $objects)); return IcingaApiCommand::create($endpoint, $data); } public function renderDeleteComment(DeleteCommentCommand $command): IcingaApiCommand { $comments = []; foreach ($command->getObjects() as $object) { $comments[] = $object->name; } $endpoint = 'actions/remove-comment'; $data = [ 'author' => $command->getAuthor(), 'comments' => $comments ]; return IcingaApiCommand::create($endpoint, $data); } public function renderDeleteDowntime(DeleteDowntimeCommand $command): IcingaApiCommand { $downtimes = []; foreach ($command->getObjects() as $object) { $downtimes[] = $object->name; } $endpoint = 'actions/remove-downtime'; $data = [ 'author' => $command->getAuthor(), 'downtimes' => $downtimes ]; return IcingaApiCommand::create($endpoint, $data); } public function renderRemoveAcknowledgement(RemoveAcknowledgementCommand $command): IcingaApiCommand { $endpoint = 'actions/remove-acknowledgement'; $data = ['author' => $command->getAuthor()]; $this->applyFilter($data, $command->getObjects()); return IcingaApiCommand::create($endpoint, $data); } public function renderToggleInstanceFeature(ToggleInstanceFeatureCommand $command): IcingaApiCommand { $endpoint = 'objects/icingaapplications/' . $this->getApp(); switch ($command->getFeature()) { case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_HOST_CHECKS: $attr = 'enable_host_checks'; break; case ToggleInstanceFeatureCommand::FEATURE_ACTIVE_SERVICE_CHECKS: $attr = 'enable_service_checks'; break; case ToggleInstanceFeatureCommand::FEATURE_EVENT_HANDLERS: $attr = 'enable_event_handlers'; break; case ToggleInstanceFeatureCommand::FEATURE_FLAP_DETECTION: $attr = 'enable_flapping'; break; case ToggleInstanceFeatureCommand::FEATURE_NOTIFICATIONS: $attr = 'enable_notifications'; break; case ToggleInstanceFeatureCommand::FEATURE_PERFORMANCE_DATA: $attr = 'enable_perfdata'; break; default: throw new InvalidArgumentException($command->getFeature()); } $data = [ 'attrs' => [ $attr => $command->getEnabled() ] ]; return IcingaApiCommand::create($endpoint, $data); } } icingadb-web-1.4.0/library/Icingadb/Command/Renderer/IcingaCommandRendererInterface.php000066400000000000000000000004101516124654200310320ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Renderer; /** * Interface for Icinga command renderer */ interface IcingaCommandRendererInterface { } icingadb-web-1.4.0/library/Icingadb/Command/Transport/000077500000000000000000000000001516124654200225535ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Command/Transport/ApiCommandException.php000066400000000000000000000005041516124654200271520ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Transport; use Icinga\Exception\IcingaException; /** * Exception thrown if a command was not successful */ class ApiCommandException extends IcingaException { } icingadb-web-1.4.0/library/Icingadb/Command/Transport/ApiCommandTransport.php000066400000000000000000000223341516124654200272150ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Transport; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use Icinga\Application\Hook\AuditHook; use Icinga\Application\Logger; use Icinga\Exception\Json\JsonDecodeException; use Icinga\Module\Icingadb\Command\IcingaApiCommand; use Icinga\Module\Icingadb\Command\IcingaCommand; use Icinga\Module\Icingadb\Command\Renderer\IcingaApiCommandRenderer; use Icinga\Util\Json; /** * Command transport over Icinga 2's REST API */ class ApiCommandTransport implements CommandTransportInterface { /** @var int Used timeout when sending a command */ protected const SEND_TIMEOUT = 15; /** * Transport identifier */ public const TRANSPORT = 'api'; /** * API host * * @var string */ protected $host; /** * API password * * @var string */ protected $password; /** * API port * * @var int */ protected $port = 5665; /** * Command renderer * * @var IcingaApiCommandRenderer */ protected $renderer; /** * API username * * @var string */ protected $username; /** * Create a new API command transport */ public function __construct() { $this->renderer = new IcingaApiCommandRenderer(); } /** * Set the name of the Icinga application object * * @param string $app * * @return $this */ public function setApp(string $app): self { $this->renderer->setApp($app); return $this; } /** * Get the API host * * @return string */ public function getHost(): string { if ($this->host === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->host; } /** * Set the API host * * @param string $host * * @return $this */ public function setHost(string $host): self { $this->host = $host; return $this; } /** * Get the API password * * @return string */ public function getPassword(): string { if ($this->password === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->password; } /** * Set the API password * * @param string $password * * @return $this */ public function setPassword(string $password): self { $this->password = $password; return $this; } /** * Get the API port * * @return int */ public function getPort(): int { return $this->port; } /** * Set the API port * * @param int $port * * @return $this */ public function setPort(int $port): self { $this->port = $port; return $this; } /** * Get the API username * * @return string */ public function getUsername(): string { if ($this->username === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->username; } /** * Set the API username * * @param string $username * * @return $this */ public function setUsername(string $username): self { $this->username = $username; return $this; } /** * Get URI for endpoint * * @param string $endpoint * * @return string */ protected function getUriFor(string $endpoint): string { return sprintf('https://%s:%u/v1/%s', $this->getHost(), $this->getPort(), $endpoint); } /** * Send the given command to the icinga2's REST API * * @param IcingaApiCommand $command * * @return mixed */ protected function sendCommand(IcingaApiCommand $command) { Logger::debug( 'Sending Icinga command "%s" to the API "%s:%u"', $command->getEndpoint(), $this->getHost(), $this->getPort() ); $data = $command->getData(); $payload = Json::encode($data); AuditHook::logActivity( 'monitoring/command', "Issued command {$command->getEndpoint()} with the following payload: $payload", $data ); $headers = ['Accept' => 'application/json']; if ($command->getMethod() !== 'POST') { $headers['X-HTTP-Method-Override'] = $command->getMethod(); } try { $response = (new Client(['timeout' => static::SEND_TIMEOUT])) ->post($this->getUriFor($command->getEndpoint()), [ 'auth' => [$this->getUsername(), $this->getPassword()], 'headers' => $headers, 'json' => $command->getData(), 'http_errors' => false, 'verify' => false ]); } catch (GuzzleException $e) { if (str_starts_with(ltrim($e->getMessage()), 'cURL error 28:')) { throw new ApiCommandException(t( 'No response from the Icinga 2 API received after %d seconds.' . ' Please make sure the action has not been performed, before retrying' ), static::SEND_TIMEOUT, $e); } throw (new CommandTransportException( 'Can\'t connect to the Icinga 2 API: %u %s', $e->getCode(), $e->getMessage() ))->setCommand($command); } try { $responseData = Json::decode((string) $response->getBody(), true); } catch (JsonDecodeException $e) { throw new CommandTransportException( 'Got invalid JSON response from the Icinga 2 API: %s', $e->getMessage() ); } if (! isset($responseData['results']) || empty($responseData['results'])) { if (isset($responseData['error'])) { throw new ApiCommandException( 'Can\'t send external Icinga command: %u %s', $responseData['error'], $responseData['status'] ); } return; } $errorResult = $responseData['results'][0]; if (isset($errorResult['code']) && ($errorResult['code'] < 200 || $errorResult['code'] >= 300)) { throw new ApiCommandException( 'Can\'t send external Icinga command: %u %s', $errorResult['code'], $errorResult['status'] ); } return $responseData['results']; } /** * Send the Icinga command over the Icinga 2 API * * @param IcingaCommand|IcingaApiCommand $command * @param ?int $now * * @throws CommandTransportException * * @return mixed */ public function send(IcingaCommand|IcingaApiCommand $command, ?int $now = null) { if ($command instanceof IcingaCommand) { $command = $this->renderer->render($command); } return $this->sendCommand($command); } /** * Try to connect to the API * * @return void * * @throws CommandTransportException In case the connection was not successful */ public function probe() { try { $response = (new Client(['timeout' => static::SEND_TIMEOUT])) ->get($this->getUriFor(''), [ 'auth' => [$this->getUsername(), $this->getPassword()], 'headers' => ['Accept' => 'application/json'], 'http_errors' => false, 'verify' => false ]); } catch (GuzzleException $e) { throw new CommandTransportException( 'Can\'t connect to the Icinga 2 API: %u %s', $e->getCode(), $e->getMessage() ); } try { $responseData = Json::decode((string) $response->getBody(), true); } catch (JsonDecodeException $e) { throw new CommandTransportException( 'Got invalid JSON response from the Icinga 2 API: %s', $e->getMessage() ); } if (! isset($responseData['results']) || empty($responseData['results'])) { throw new CommandTransportException( 'Got invalid response from the Icinga 2 API: %s', JSON::encode($responseData) ); } $result = array_pop($responseData['results']); if (! isset($result['user']) || $result['user'] !== $this->getUsername()) { throw new CommandTransportException( 'Got invalid response from the Icinga 2 API: %s', JSON::encode($responseData) ); } } } icingadb-web-1.4.0/library/Icingadb/Command/Transport/CommandTransport.php000066400000000000000000000151501516124654200265610ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Transport; use Icinga\Application\Config; use Icinga\Application\Logger; use Icinga\Data\ConfigObject; use Icinga\Exception\ConfigurationError; use Icinga\Module\Icingadb\Command\IcingaCommand; use Icinga\Module\Icingadb\Command\Object\ObjectsCommand; /** * Command transport */ class CommandTransport implements CommandTransportInterface { /** * Transport configuration * * @var Config */ protected static $config; /** * Get transport configuration * * @return Config * * @throws ConfigurationError */ public static function getConfig(): Config { if (static::$config === null) { $config = Config::module('icingadb', 'commandtransports'); if ($config->isEmpty()) { throw new ConfigurationError( t('No command transports have been configured in "%s".'), $config->getConfigFile() ); } static::$config = $config; } return static::$config; } /** * Create a transport from config * * @param ConfigObject $config * * @return ApiCommandTransport * * @throws ConfigurationError */ public static function createTransport(ConfigObject $config): ApiCommandTransport { $config = clone $config; switch (strtolower($config->transport ?? '')) { case ApiCommandTransport::TRANSPORT: $transport = new ApiCommandTransport(); break; default: throw new ConfigurationError( t('Cannot create command transport "%s". Invalid transport defined in "%s". Use one of: %s.'), $config->transport, static::getConfig()->getConfigFile(), join(', ', [ApiCommandTransport::TRANSPORT]) ); } unset($config->transport); foreach ($config as $key => $value) { $method = 'set' . ucfirst($key); if (! method_exists($transport, $method)) { // Ignore settings from config that don't have a setter on the transport instead of throwing an // exception here because the transport should throw an exception if it's not fully set up // when being about to send a command continue; } $transport->$method($value); } return $transport; } /** * Send the given command over an appropriate Icinga command transport * * This will try one configured transport after another until the command has been successfully sent. * * @param IcingaCommand $command The command to send * @param ?int $now Timestamp of the command or null for now * * @return mixed * * @throws CommandTransportException If sending the Icinga command failed */ public function send(IcingaCommand $command, ?int $now = null) { $errors = []; $results = []; $retryCommand = null; foreach (static::getConfig() as $name => $transportConfig) { $transport = static::createTransport($transportConfig); if ($command instanceof ObjectsCommand && $command->getChunkSize() > 0) { $objects = $command->getObjects(); if ($retryCommand !== null) { try { $results[] = $transport->send($retryCommand, $now); } catch (CommandTransportException) { // It failed prior, so no need to log it again continue; } $retryCommand = null; } else { if ($objects->key() === null) { // We traverse the iterator manually here, so we have to rewind it before the first iteration. // That should be the case if the current key is null. May fail if an iterator explicitly yields // null as the key, but I want to see a justified use case for that… $objects->rewind(); } } while ($objects->valid()) { $batchCommand = clone $command; $batchCommand->setObjects( new \LimitIterator(new \NoRewindIterator($objects), 0, $command->getChunkSize()) ); try { $results[] = $transport->send($batchCommand, $now); } catch (CommandTransportException $e) { Logger::error($e); $errors[] = sprintf('%s: %s.', $name, rtrim($e->getMessage(), '.')); $retryCommand = $e->getCommand(); if ($retryCommand !== null) { continue 2; } else { // Non-recoverable error, so stop trying to send further commands break 2; } } } return $results; } elseif ($retryCommand !== null) { try { $result = $transport->send($retryCommand, $now); } catch (CommandTransportException) { // It failed prior, so no need to log it again continue; } return $result; } else { try { $result = $transport->send($command, $now); } catch (CommandTransportException $e) { Logger::error($e); $errors[] = sprintf('%s: %s.', $name, rtrim($e->getMessage(), '.')); $retryCommand = $e->getCommand(); if ($retryCommand !== null) { continue; // Try the next transport } else { break; } } return $result; // The command was successfully sent } } if (! empty($errors)) { throw new CommandTransportException(implode("\n", $errors)); } throw new CommandTransportException(t( 'Failed to send external Icinga command. No transport has been configured' . ' for this instance. Please contact your Icinga Web administrator.' )); } } icingadb-web-1.4.0/library/Icingadb/Command/Transport/CommandTransportConfig.php000066400000000000000000000012611516124654200277050ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Transport; use Icinga\Repository\IniRepository; class CommandTransportConfig extends IniRepository { protected $configs = [ 'transport' => [ 'name' => 'commandtransports', 'module' => 'icingadb', 'keyColumn' => 'name' ] ]; protected $queryColumns = [ 'transport' => [ 'name', 'transport', // API options 'host', 'port', 'username', 'password' ] ]; } icingadb-web-1.4.0/library/Icingadb/Command/Transport/CommandTransportException.php000066400000000000000000000017371516124654200304460ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Transport; use Icinga\Exception\IcingaException; /** * Exception thrown if a command was not sent */ class CommandTransportException extends IcingaException { /** @var mixed The command that was not sent */ private mixed $command = null; /** * Set the command that was not sent * * This will be passed to the next transport in the chain. * Make sure the transport accepts this type in {@see CommandTransportInterface::send()}. * * @param mixed $command * * @return $this */ public function setCommand(mixed $command): static { $this->command = $command; return $this; } /** * Get the command that was not sent * * @return mixed */ public function getCommand(): mixed { return $this->command; } } icingadb-web-1.4.0/library/Icingadb/Command/Transport/CommandTransportInterface.php000066400000000000000000000012431516124654200304000ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Command\Transport; use Icinga\Module\Icingadb\Command\IcingaCommand; /** * Interface for Icinga command transports */ interface CommandTransportInterface { /** * Send an Icinga command over the Icinga command transport * * @param IcingaCommand $command The command to send * @param ?int $now Timestamp of the command or null for now * * @throws CommandTransportException If sending the Icinga command failed */ public function send(IcingaCommand $command, ?int $now = null); } icingadb-web-1.4.0/library/Icingadb/Common/000077500000000000000000000000001516124654200204315ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Common/Auth.php000066400000000000000000000515111516124654200220460ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Icinga\Application\Logger; use Icinga\Authentication\Auth as IcingaAuth; use Icinga\Exception\ConfigurationError; use Icinga\Module\Icingadb\Authentication\ObjectAuthorization; use Icinga\Security\SecurityException; use Icinga\Util\StringHelper; use ipl\Orm\Compat\FilterProcessor; use ipl\Orm\Model; use ipl\Orm\Query; use ipl\Orm\UnionQuery; use ipl\Sql\Expression; use ipl\Stdlib\Filter; use ipl\Web\Filter\QueryString; use ipl\Web\Filter\Renderer; trait Auth { public function getAuth(): IcingaAuth { return IcingaAuth::getInstance(); } /** * Check whether access to the given route is permitted * * @param string $name * * @return bool */ public function isPermittedRoute(string $name): bool { if ($this->getAuth()->getUser()->isUnrestricted()) { return true; } // The empty array is for PHP pre 7.4, older versions require at least a single param for array_merge $routeDenylist = array_flip(array_merge([], ...array_map(function ($restriction) { return StringHelper::trimSplit($restriction); }, $this->getAuth()->getRestrictions('icingadb/denylist/routes')))); if (! array_key_exists($name, $routeDenylist)) { return true; } return false; } /** * Check whether the permission is granted on the object * * @param string $permission * @param Model $object * * @return bool */ public function isGrantedOn(string $permission, Model $object): bool { if ($this->getAuth()->getUser()->isUnrestricted()) { return $this->getAuth()->hasPermission($permission); } return ObjectAuthorization::grantsOn($permission, $object); } /** * Check whether the permission is granted on objects matching the type and filter * * The check will be performed on every object matching the filter. Though the result * only allows to determine whether the permission is granted on **any** or *none* * of the objects in question. Any subsequent call to {@see Auth::isGrantedOn} will * make use of the underlying results the check has determined in order to avoid * unnecessary queries. * * @param string $permission * @param string $type * @param Filter\Rule $filter * @param bool $cache Pass `false` to not perform the check on every object * * @return bool */ public function isGrantedOnType(string $permission, string $type, Filter\Rule $filter, bool $cache = true): bool { if ($this->getAuth()->getUser()->isUnrestricted()) { return $this->getAuth()->hasPermission($permission); } return ObjectAuthorization::grantsOnType($permission, $type, $filter, $cache); } /** * Check whether the filter matches the given object * * @param string $queryString * @param Model $object * * @return bool */ public function isMatchedOn(string $queryString, Model $object): bool { return ObjectAuthorization::matchesOn($queryString, $object); } /** * Assert that the given filter does not contain any column restrictions * * @param Filter\Rule $filter The filter to check * * @return void * * @throws SecurityException */ public function assertColumnRestrictions(Filter\Rule $filter): void { if ($this->getAuth()->getUser() === null || $this->getAuth()->getUser()->isUnrestricted()) { return; } $forbiddenVars = Filter::none(); foreach ($this->getAuth()->getUser()->getRoles() as $role) { if (($restriction = $role->getRestrictions('icingadb/denylist/variables'))) { foreach (explode(',', $restriction) as $value) { $forbiddenVars->add(Filter::like('name', trim($value))->ignoreCase()); } } if (($restriction = $role->getRestrictions('icingadb/protect/variables'))) { foreach (explode(',', $restriction) as $value) { $forbiddenVars->add(Filter::like('name', trim($value))->ignoreCase()); } } } if ($forbiddenVars->isEmpty()) { return; } $checkVars = static function (Filter\Rule $filter) use ($forbiddenVars, &$checkVars) { if ($filter instanceof Filter\Chain) { foreach ($filter as $rule) { $checkVars($rule); } } elseif ( ! $filter->metaData()->get('_isRestriction', false) && preg_match('/^(?:host|service)\.vars\.(.*)/i', $filter->getColumn(), $matches) ) { if (! Filter::match($forbiddenVars, ['name' => $matches[1]])) { throw new SecurityException( 'The variable "%s" is not allowed to be queried.', $matches[1] ); } } }; $checkVars($filter); } /** * Apply Icinga DB Web's restrictions depending on what is queried * * This will apply `icingadb/filter/objects` in any case. `icingadb/filter/services` is only * applied to queries fetching services and `icingadb/filter/hosts` is applied to queries * fetching either hosts or services. It also applies custom variable restrictions and * obfuscations. (`icingadb/denylist/variables` and `icingadb/protect/variables`) * * @param Query $query * * @return void */ public function applyRestrictions(Query $query) { if ($this->getAuth()->getUser()->isUnrestricted()) { return; } if ($query instanceof UnionQuery) { $queries = $query->getUnions(); $forceGroupFilterOptimization = true; } else { $queries = [$query]; $forceGroupFilterOptimization = false; } foreach ($queries as $query) { $relations = [$query->getModel()->getTableName()]; foreach ($query->getWith() as $relationPath => $relation) { $relations[$relationPath] = $relation->getTarget()->getTableName(); } $customVarRelationName = array_search('customvar_flat', $relations, true); $applyServiceRestriction = $relations[0] === 'dependency_node' || in_array('service', $relations, true); $applyHostRestriction = $relations[0] === 'dependency_node' || in_array('host', $relations, true) // Hosts and services have a special relation as a service can't exist without its host. // Hence why the hosts restriction is also applied if only services are queried. || $applyServiceRestriction; // Redundancy groups have no relation to anything in order to be subject // for authorization, so they must be exempt from the respective filters. $skipRedundancyGroups = $relations[0] === 'dependency_node'; $hostStateRelation = array_search('host_state', $relations, true); $serviceStateRelation = array_search('service_state', $relations, true); $resolver = $query->getResolver(); $queryFilter = Filter::any(); $forbiddenVars = Filter::none(); $obfuscationRules = Filter::all(); foreach ($this->getAuth()->getUser()->getRoles() as $role) { $roleFilter = Filter::all(); if ($customVarRelationName !== false) { if (($restriction = $role->getRestrictions('icingadb/denylist/variables'))) { $this->flattenSemanticallyEqualRules($forbiddenVars, $this->parseDenylist( $restriction, $customVarRelationName ? $resolver->qualifyColumn('flatname', $customVarRelationName) : 'flatname' )); } if (($restriction = $role->getRestrictions('icingadb/protect/variables'))) { $obfuscationRules->add($this->parseDenylist( $restriction, $customVarRelationName ? $resolver->qualifyColumn('flatname', $customVarRelationName) : 'flatname' )); } } if ($customVarRelationName === false || count($relations) > 1) { if (($restriction = $role->getRestrictions('icingadb/filter/objects'))) { if ($skipRedundancyGroups) { $roleFilter->add(Filter::any( Filter::all( Filter::unlike('host_id', '*'), Filter::unlike('service_id', '*') ), $this->parseRestriction($restriction, 'icingadb/filter/objects') )); } else { $roleFilter->add($this->parseRestriction($restriction, 'icingadb/filter/objects')); } } if ($applyHostRestriction && ($restriction = $role->getRestrictions('icingadb/filter/hosts'))) { $hostFilter = $this->parseRestriction($restriction, 'icingadb/filter/hosts'); if ($forceGroupFilterOptimization) { $this->forceQueryOptimization($hostFilter, 'hostgroup.name'); } if ($skipRedundancyGroups) { $roleFilter->add(Filter::any(Filter::unlike('host_id', '*'), $hostFilter)); } else { $roleFilter->add($hostFilter); } } if ( $applyServiceRestriction && ($restriction = $role->getRestrictions('icingadb/filter/services')) ) { $serviceFilter = $this->parseRestriction($restriction, 'icingadb/filter/services'); if ($forceGroupFilterOptimization) { $this->forceQueryOptimization($serviceFilter, 'servicegroup.name'); } $roleFilter->add(Filter::any(Filter::unlike('service.id', '*'), $serviceFilter)); } } if (! $roleFilter->isEmpty()) { if (Logger::getInstance()->getLevel() === Logger::DEBUG) { Logger::debug( 'Preparing restrictions for user %s with role %s: %s; Current query filter: %s', $this->getAuth()->getUser()->getUsername(), $role->getName(), (new Renderer($roleFilter))->setStrict()->render(), (new Renderer($queryFilter))->setStrict()->render() ); } if (! $this->flattenSemanticallyEqualRules($queryFilter, $roleFilter)) { $queryFilter->add($roleFilter); } } } if (! $this->getAuth()->hasPermission('icingadb/object/show-source')) { // In case the user does not have permission to see the object's `Source` tab, then the user must be // restricted from accessing the executed command for the object. $columns = $query->getColumns(); $commandColumns = []; if ($hostStateRelation !== false) { $commandColumns[] = $resolver->qualifyColumn('check_commandline', $hostStateRelation); } if ($serviceStateRelation !== false) { $commandColumns[] = $resolver->qualifyColumn('check_commandline', $serviceStateRelation); } if (! empty($columns)) { foreach ($commandColumns as $commandColumn) { $commandColumnPath = array_search($commandColumn, $columns, true); if ($commandColumnPath !== false) { $columns[$commandColumn] = new Expression("'***'"); unset($columns[$commandColumnPath]); } } $query->columns($columns); } else { $query->withoutColumns($commandColumns); } } if (! $obfuscationRules->isEmpty()) { $flatvaluePath = $customVarRelationName ? $resolver->qualifyColumn('flatvalue', $customVarRelationName) : 'flatvalue'; $columns = $query->getColumns(); if (empty($columns)) { $columns = [ $customVarRelationName ? $resolver->qualifyColumn('flatname', $customVarRelationName) : 'flatname', $flatvaluePath ]; } $flatvalue = null; if (isset($columns[$flatvaluePath])) { $flatvalue = $columns[$flatvaluePath]; } else { $flatvaluePathAt = array_search($flatvaluePath, $columns, true); if ($flatvaluePathAt !== false) { $flatvalue = $columns[$flatvaluePathAt]; if (is_int($flatvaluePathAt)) { unset($columns[$flatvaluePathAt]); } else { $flatvaluePath = $flatvaluePathAt; } } } if ($flatvalue !== null) { // TODO: The four lines below are needed because there is still no way to postpone filter column // qualification. (i.e. Just like the expression, filter rules need to be handled the same // so that their columns are qualified lazily when assembling the query) $queryClone = clone $query; $queryClone->getSelectBase()->resetWhere(); FilterProcessor::apply($obfuscationRules, $queryClone); $where = $queryClone->getSelectBase()->getWhere(); $values = []; $rendered = $query->getDb()->getQueryBuilder()->buildCondition($where, $values); $columns[$flatvaluePath] = new Expression( "CASE WHEN (" . $rendered . ") THEN (%s) ELSE '***' END", [$flatvalue], ...$values ); $query->columns($columns); } } if (Logger::getInstance()->getLevel() === Logger::DEBUG) { Logger::debug( 'Final restrictions for user %s: %s', $this->getAuth()->getUser()->getUsername(), (new Renderer($queryFilter))->setStrict()->render() ); } $query->filter($queryFilter) ->filter($forbiddenVars); } } /** * Parse the given restriction * * @param string $queryString * @param string $restriction The name of the restriction * * @return Filter\Rule */ protected function parseRestriction(string $queryString, string $restriction): Filter\Rule { $allowedColumns = [ 'host.name', 'hostgroup.name', 'host.user.name', 'host.usergroup.name', 'service.name', 'servicegroup.name', 'service.user.name', 'service.usergroup.name', '(host|service).vars.' => function ($c) { return preg_match('/^(?:host|service)\.vars\./i', $c); } ]; return QueryString::fromString($queryString) ->on( QueryString::ON_CONDITION, function (Filter\Condition $condition) use ( $restriction, $queryString, $allowedColumns ) { foreach ($allowedColumns as $column) { if (is_callable($column)) { if ($column($condition->getColumn())) { $condition->metaData()->set('_isRestriction', true); return; } } elseif ($column === $condition->getColumn()) { $condition->metaData()->set('_isRestriction', true); return; } } throw new ConfigurationError( t( 'Cannot apply restriction %s using the filter %s.' . ' You can only use the following columns: %s' ), $restriction, $queryString, join( ', ', array_map( function ($k, $v) { return is_string($k) ? $k : $v; }, array_keys($allowedColumns), $allowedColumns ) ) ); } )->parse(); } /** * Parse the given denylist * * @param string $denylist Comma separated list of column names * @param string $column The column which should not equal any of the denylisted names * * @return Filter\None */ protected function parseDenylist(string $denylist, string $column): Filter\None { $filter = Filter::none(); foreach (explode(',', $denylist) as $value) { $filter->add(Filter::like($column, trim($value))); } return $filter; } /** * Flatten the given rule into the given chain if they are semantically equal * * This is needed as the same filter may be added multiple times to the same query, which leads to multiple nested * chains of the same type. This method flattens those chains into a single one, which allows for better * optimization and therefore enhanced efficiency. * * @param Filter\Chain $to * @param Filter\Chain $from * * @return bool Whether the $from rule has been added to the $to chain */ private function flattenSemanticallyEqualRules(Filter\Chain $to, Filter\Chain $from): bool { $transfer = $from instanceof $to || (! $from instanceof Filter\None && $from->count() === 1); foreach (iterator_to_array($from) as $rule) { if ($rule instanceof Filter\Chain) { if ($transfer) { if (! $this->flattenSemanticallyEqualRules($to, $rule)) { $to->add($rule); } $from->remove($rule); } elseif ($this->flattenSemanticallyEqualRules($from, $rule)) { $from->remove($rule); } } elseif ($transfer) { $to->add($rule); $from->remove($rule); } } if (! $to->has($from)) { if (! $from->isEmpty()) { $to->add($from); } return true; } elseif ($from->isEmpty()) { $to->remove($from); } return false; } /** * Force query optimization on the given service/host filter rule * * Applies forceOptimization, when the given filter rule contains the given filter column * * @param Filter\Rule $filterRule * @param string $filterColumn * * @return void */ protected function forceQueryOptimization(Filter\Rule $filterRule, string $filterColumn) { // TODO: This is really a very poor solution is therefore only a quick fix. // We need to somehow manage to make this more enjoyable and creative! if ($filterRule instanceof Filter\Chain) { foreach ($filterRule as $rule) { $this->forceQueryOptimization($rule, $filterColumn); } } elseif ($filterRule->getColumn() === $filterColumn) { $filterRule->metaData()->set('forceOptimization', true); } } } icingadb-web-1.4.0/library/Icingadb/Common/Backend.php000066400000000000000000000147501516124654200225000ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Icinga\Application\Config as AppConfig; use Icinga\Data\ResourceFactory; use Icinga\Module\Icingadb\Model\Schema; use ipl\Sql\Adapter\Pgsql; use ipl\Sql\Config as SqlConfig; use ipl\Sql\Connection; use ipl\Sql\Expression; use ipl\Sql\QueryBuilder; use ipl\Sql\Select; use PDO; use Pdo\Mysql; /** * Singleton providing access to the Icinga DB and Redis */ final class Backend { /** @var ?Connection */ private static $db; /** @var ?int */ private static $schemaVersion; /** @var ?IcingaRedis */ private static $redis; /** @var ?bool Whether the current Icinga DB version supports dependencies */ private static $supportsDependencies; /** * Set the connection to the Icinga DB * * Usually not required, as the connection is created on demand. Useful for testing. * * @param Connection $db * * @return void */ public static function setDb(Connection $db): void { self::$db = $db; } /** * Get the connection to the Icinga DB * * @return Connection */ public static function getDb(): Connection { if (self::$db === null) { $config = new SqlConfig(ResourceFactory::getResourceConfig( AppConfig::module('icingadb')->get('icingadb', 'resource') )); $config->options = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ]; if ($config->db === 'mysql') { $config->options[Mysql::ATTR_INIT_COMMAND] = "SET SESSION SQL_MODE='STRICT_TRANS_TABLES" . ",NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'"; } self::$db = new Connection($config); $adapter = self::$db->getAdapter(); if ($adapter instanceof Pgsql) { $quoted = $adapter->quoteIdentifier('user'); self::$db->getQueryBuilder() ->on(QueryBuilder::ON_SELECT_ASSEMBLED, function (&$sql) use ($quoted) { // user is a reserved key word in PostgreSQL, so we need to quote it. // TODO(lippserd): This is pretty hacky, // reconsider how to properly implement identifier quoting. $sql = preg_replace('/user$/', sprintf(' %s', $quoted), $sql); $sql = str_replace( [' user ', ' user.', '(user.'], [ sprintf(' %s ', $quoted), sprintf(' %s.', $quoted), sprintf('(%s.', $quoted), ], $sql ); }) ->on(QueryBuilder::ON_ASSEMBLE_SELECT, function (Select $select) { // For SELECT DISTINCT, all ORDER BY columns must appear in SELECT list. if (! $select->getDistinct() || ! $select->hasOrderBy()) { return; } $candidates = []; foreach ($select->getOrderBy() as list($columnOrAlias, $_)) { if ($columnOrAlias instanceof Expression) { // Expressions can be and include anything, // also columns that aren't already part of the SELECT list, // so we're not trying to guess anything here. // Such expressions must be in the SELECT list if necessary and // referenced manually with an alias in ORDER BY. continue; } $candidates[$columnOrAlias] = true; } foreach ($select->getColumns() as $alias => $column) { if (is_int($alias)) { if ($column instanceof Expression) { // This is the complement to the above consideration. // If it is an unaliased expression, ignore it. continue; } } else { unset($candidates[$alias]); } if (! $column instanceof Expression) { unset($candidates[$column]); } } if (! empty($candidates)) { $select->columns(array_keys($candidates)); } }); } } return self::$db; } /** * Get the schema version of the Icinga DB * * @return int */ public static function getDbSchemaVersion(): int { if (self::$schemaVersion === null) { self::$schemaVersion = Schema::on(self::getDb()) ->columns('version') ->first() ->version ?? 0; } return self::$schemaVersion; } /** * Set the connection to the Icinga Redis * * Usually not required, as the connection is created on demand. Useful for testing. * * @param IcingaRedis $redis * * @return void */ public static function setRedis(IcingaRedis $redis): void { self::$redis = $redis; } /** * Get the connection to the Icinga Redis * * @return IcingaRedis */ public static function getRedis(): IcingaRedis { if (self::$redis === null) { self::$redis = new IcingaRedis(); } return self::$redis; } /** * Whether the current Icinga DB version supports dependencies * * @return bool */ public static function supportsDependencies(): bool { if (self::$supportsDependencies === null) { if (self::getDb()->getAdapter() instanceof Pgsql) { self::$supportsDependencies = self::getDbSchemaVersion() >= 5; } else { self::$supportsDependencies = self::getDbSchemaVersion() >= 7; } } return self::$supportsDependencies; } } icingadb-web-1.4.0/library/Icingadb/Common/BaseFilter.php000066400000000000000000000004751516124654200231700ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; /** * @deprecated Use {@see \ipl\Stdlib\BaseFilter} instead. This will be removed with version 1.1 */ trait BaseFilter { use \ipl\Stdlib\BaseFilter; } icingadb-web-1.4.0/library/Icingadb/Common/BaseStatusBar.php000066400000000000000000000024551516124654200236530ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Icinga\Module\Icingadb\Model\HoststateSummary; use Icinga\Module\Icingadb\Model\ServicestateSummary; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Orm\Model; use ipl\Stdlib\BaseFilter; abstract class BaseStatusBar extends BaseHtmlElement { use BaseFilter; /** @var ServicestateSummary|HoststateSummary */ protected $summary; protected $tag = 'div'; protected $defaultAttributes = ['class' => 'status-bar']; /** * Create a host or service status bar * * @param ServicestateSummary|HoststateSummary $summary */ public function __construct($summary) { $this->summary = $summary; } abstract protected function assembleTotal(BaseHtmlElement $total): void; abstract protected function createStateBadges(): BaseHtmlElement; protected function createCount(): BaseHtmlElement { $total = Html::tag('span', ['class' => 'item-count']); $this->assembleTotal($total); return $total; } protected function assemble(): void { $this->add([ $this->createCount(), $this->createStateBadges() ]); } } icingadb-web-1.4.0/library/Icingadb/Common/CommandActions.php000066400000000000000000000241651516124654200240510ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Icinga\Exception\Http\HttpNotFoundException; use Icinga\Module\Icingadb\Forms\Command\CommandForm; use Icinga\Module\Icingadb\Forms\Command\Object\AcknowledgeProblemForm; use Icinga\Module\Icingadb\Forms\Command\Object\AddCommentForm; use Icinga\Module\Icingadb\Forms\Command\Object\CheckNowForm; use Icinga\Module\Icingadb\Forms\Command\Object\ProcessCheckResultForm; use Icinga\Module\Icingadb\Forms\Command\Object\RemoveAcknowledgementForm; use Icinga\Module\Icingadb\Forms\Command\Object\ScheduleCheckForm; use Icinga\Module\Icingadb\Forms\Command\Object\ScheduleHostDowntimeForm; use Icinga\Module\Icingadb\Forms\Command\Object\ScheduleServiceDowntimeForm; use Icinga\Module\Icingadb\Forms\Command\Object\SendCustomNotificationForm; use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; use Icinga\Security\SecurityException; use Icinga\Util\Environment; use Icinga\Web\Notification; use ipl\Orm\Model; use ipl\Orm\Query; use ipl\Web\Url; /** * Trait CommandActions */ trait CommandActions { /** @var null|Query|array $commandTargets */ protected $commandTargets; /** @var Model $commandTargetModel */ protected $commandTargetModel; /** * Get url to view command targets, used as redirection target * * @return Url */ abstract protected function getCommandTargetsUrl(): Url; /** * Get status of toggleable features * * @return object */ protected function getFeatureStatus() { } /** * Fetch command targets * * @return Query|array */ abstract protected function fetchCommandTargets(); /** * Get command targets * * @return Query|array */ protected function getCommandTargets() { if (! isset($this->commandTargets)) { $this->commandTargets = $this->fetchCommandTargets(); } return $this->commandTargets; } /** * Get the model of the command targets * * @return Model * @throws HttpNotFoundException If no command targets were found */ protected function getCommandTargetModel(): Model { if (! isset($this->commandTargetModel)) { $commandTargets = $this->getCommandTargets(); if (empty($commandTargets) || count($commandTargets) === 0) { throw new HttpNotFoundException('No command targets found'); } if (is_array($commandTargets)) { $this->commandTargetModel = $commandTargets[0]; } elseif ($commandTargets->count() > 0) { $this->commandTargetModel = $commandTargets->getModel(); } } return $this->commandTargetModel; } /** * Check whether the permission is granted on any of the command targets * * @param string $permission * * @return bool */ protected function isGrantedOnCommandTargets(string $permission): bool { $commandTargets = $this->getCommandTargets(); if (is_array($commandTargets)) { foreach ($commandTargets as $commandTarget) { if ($this->isGrantedOn($permission, $commandTarget)) { return true; } } return false; } return $this->isGrantedOnType( $permission, $this->getCommandTargetModel()->getTableName(), $commandTargets->getFilter() ); } /** * Assert that the permission is granted on any of the command targets * * @param string $permission * * @throws SecurityException */ protected function assertIsGrantedOnCommandTargets(string $permission) { if (! $this->isGrantedOnCommandTargets($permission)) { throw new SecurityException('No permission for %s', $permission); } } /** * Handle and register the given command form * * @param string|CommandForm $form * * @return void */ protected function handleCommandForm($form) { $isXhr = $this->getRequest()->isXmlHttpRequest(); $isApi = $this->getRequest()->isApiRequest(); if ($isXhr && $isApi) { // Prevents the framework already, this is just a fail-safe $this->httpBadRequest('Responding with JSON during a Web request is not supported'); } // Bulk operations may require more memory and time Environment::raiseMemoryLimit(); Environment::raiseExecutionTime(); if (is_string($form)) { /** @var CommandForm $form */ $form = new $form(); } $form->setObjects($this->getCommandTargets()); if (! $isApi || $isXhr) { $this->handleWebRequest($form); } else { $this->handleApiRequest($form); } } /** * Handle a Web request for the given form * * @param CommandForm $form * * @return void */ protected function handleWebRequest(CommandForm $form): void { $actionUrl = $this->getRequest()->getUrl(); if ($this->view->compact) { $actionUrl = clone $actionUrl; // TODO: This solves https://github.com/Icinga/icingadb-web/issues/124 but I'd like to omit this // entirely. I think it should be solved like https://github.com/Icinga/icingaweb2/pull/4300 so // that a request's url object still has params like showCompact and _dev $actionUrl->getParams()->add('showCompact', true); } $form->setAction($actionUrl->getAbsoluteUrl()); $form->on($form::ON_SUCCESS, function () { // This forces the column to reload nearly instantly after the redirect // and ensures the effect of the command is visible to the user asap $this->getResponse()->setAutoRefreshInterval(1); $this->redirectNow($this->getCommandTargetsUrl()); }); $form->handleRequest($this->getServerRequest()); $this->addContent($form); } /** * Handle an API request for the given form * * @param CommandForm $form * * @return never */ protected function handleApiRequest(CommandForm $form) { $form->setIsApiTarget(); $form->on($form::ON_SUCCESS, function () { $this->getResponse() ->json() ->setSuccessData(Notification::getInstance()->popMessages()) ->sendResponse(); }); $form->handleRequest($this->getServerRequest()); $this->assertHttpMethod($form->getMethod()); $errors = []; foreach ($form->getElements() as $element) { $errors[$element->getName()] = $element->getMessages(); } $response = $this->getResponse()->json(); $response->setHttpResponseCode(422); $response->setFailData($errors) ->sendResponse(); } public function acknowledgeAction() { $this->assertIsGrantedOnCommandTargets('icingadb/command/acknowledge-problem'); $this->setTitle(t('Acknowledge Problem')); $this->handleCommandForm(AcknowledgeProblemForm::class); } public function addCommentAction() { $this->assertIsGrantedOnCommandTargets('icingadb/command/comment/add'); $this->setTitle(t('Add Comment')); $this->handleCommandForm(AddCommentForm::class); } public function checkNowAction() { if (! $this->isGrantedOnCommandTargets('icingadb/command/schedule-check/active-only')) { $this->assertIsGrantedOnCommandTargets('icingadb/command/schedule-check'); } $this->handleCommandForm(CheckNowForm::class); } public function processCheckresultAction() { $this->assertIsGrantedOnCommandTargets('icingadb/command/process-check-result'); $this->setTitle(t('Submit Passive Check Result')); $this->handleCommandForm(ProcessCheckResultForm::class); } public function removeAcknowledgementAction() { $this->assertIsGrantedOnCommandTargets('icingadb/command/remove-acknowledgement'); $this->handleCommandForm(RemoveAcknowledgementForm::class); } public function scheduleCheckAction() { if (! $this->isGrantedOnCommandTargets('icingadb/command/schedule-check/active-only')) { $this->assertIsGrantedOnCommandTargets('icingadb/command/schedule-check'); } $this->setTitle(t('Reschedule Check')); $this->handleCommandForm(ScheduleCheckForm::class); } public function scheduleDowntimeAction() { $this->assertIsGrantedOnCommandTargets('icingadb/command/downtime/schedule'); switch ($this->getCommandTargetModel()->getTableName()) { case 'host': $this->setTitle(t('Schedule Host Downtime')); $this->handleCommandForm(ScheduleHostDowntimeForm::class); break; case 'service': $this->setTitle(t('Schedule Service Downtime')); $this->handleCommandForm(ScheduleServiceDowntimeForm::class); break; } } public function sendCustomNotificationAction() { $this->assertIsGrantedOnCommandTargets('icingadb/command/send-custom-notification'); $this->setTitle(t('Send Custom Notification')); $this->handleCommandForm(SendCustomNotificationForm::class); } public function toggleFeaturesAction() { $commandObjects = $this->getCommandTargets(); $form = null; if (count($commandObjects) > 1) { $this->isGrantedOnCommandTargets('i/am-only-used/to-establish/the-object-auth-cache'); $form = new ToggleObjectFeaturesForm($this->getFeatureStatus()); } else { foreach ($commandObjects as $object) { // There's only a single result, a foreach is the most compatible way to retrieve the object $form = new ToggleObjectFeaturesForm($object); } } $this->handleCommandForm($form); } } icingadb-web-1.4.0/library/Icingadb/Common/Database.php000066400000000000000000000006031516124654200226450ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use ipl\Sql\Connection; trait Database { /** * Get the connection to the Icinga database * * @return Connection */ public function getDb(): Connection { return Backend::getDb(); } } icingadb-web-1.4.0/library/Icingadb/Common/DetailActions.php000066400000000000000000000100421516124654200236620ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use ipl\Html\BaseHtmlElement; use ipl\Stdlib\Filter; use ipl\Web\Filter\QueryString; use ipl\Web\Url; trait DetailActions { /** @var bool */ protected $detailActionsDisabled = false; /** * Set whether this list should be an action-list * * @param bool $state * * @return $this */ public function setDetailActionsDisabled(bool $state = true): self { $this->detailActionsDisabled = $state; return $this; } /** * Get whether this list should be an action-list * * @return bool */ public function getDetailActionsDisabled(): bool { return $this->detailActionsDisabled; } /** * Prepare this list as action-list * * @return $this */ public function initializeDetailActions(): self { $this->getAttributes() ->registerAttributeCallback('class', function () { return $this->getDetailActionsDisabled() ? null : 'action-list'; }) ->registerAttributeCallback('data-icinga-multiselect-count-label', function () { return $this->getDetailActionsDisabled() ? null : t('%d Item(s) selected'); }) ->registerAttributeCallback('data-icinga-multiselect-hint-label', function () { return $this->getDetailActionsDisabled() ? null : t('Use shift/cmd + click/arrow keys to select multiple items'); }); return $this; } /** * Set the url to use for multiple selected list items * * @param Url $url * * @return $this */ protected function setMultiselectUrl(Url $url): self { $this->getAttributes() ->registerAttributeCallback('data-icinga-multiselect-url', function () use ($url) { return $this->getDetailActionsDisabled() ? null : (string) $url; }); return $this; } /** * Set the url to use for a single selected list item * * @param Url $url * * @return $this */ protected function setDetailUrl(Url $url): self { $this->getAttributes() ->registerAttributeCallback('data-icinga-detail-url', function () use ($url) { return $this->getDetailActionsDisabled() ? null : (string) $url; }); return $this; } /** * Associate the given element with the given multi-selection filter * * @param BaseHtmlElement $element * @param Filter\Rule $filter * * @return $this */ public function addMultiselectFilterAttribute(BaseHtmlElement $element, Filter\Rule $filter): self { $element->getAttributes() ->registerAttributeCallback('data-icinga-multiselect-filter', function () use ($filter) { if ($this->getDetailActionsDisabled()) { return null; } $queryString = QueryString::render($filter); if ($filter instanceof Filter\Chain) { $queryString = '(' . $queryString . ')'; } return $queryString; }); return $this; } /** * Associate the given element with the given single-selection filter * * @param BaseHtmlElement $element * @param Filter\Rule $filter * * @return $this */ public function addDetailFilterAttribute(BaseHtmlElement $element, Filter\Rule $filter): self { $element->getAttributes() ->registerAttributeCallback('data-action-item', function () { return ! $this->getDetailActionsDisabled(); }) ->registerAttributeCallback('data-icinga-detail-filter', function () use ($filter) { return $this->getDetailActionsDisabled() ? null : QueryString::render($filter); }); return $this; } } icingadb-web-1.4.0/library/Icingadb/Common/HostLink.php000066400000000000000000000013351516124654200226770ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Icinga\Module\Icingadb\Model\Host; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Web\Widget\StateBall; trait HostLink { protected function createHostLink(Host $host, bool $withStateBall = false): BaseHtmlElement { $content = []; if ($withStateBall) { $content[] = new StateBall($host->state->getStateText(), StateBall::SIZE_MEDIUM); $content[] = ' '; } $content[] = $host->display_name; return Html::tag('a', ['href' => Links::host($host), 'class' => 'subject'], $content); } } icingadb-web-1.4.0/library/Icingadb/Common/HostLinks.php000066400000000000000000000043571516124654200230710ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Icinga\Module\Icingadb\Model\Host; use ipl\Web\Url; abstract class HostLinks { public static function acknowledge(Host $host): Url { return Url::fromPath('icingadb/host/acknowledge', ['name' => $host->name]); } public static function addComment(Host $host): Url { return Url::fromPath('icingadb/host/add-comment', ['name' => $host->name]); } public static function checkNow(Host $host): Url { return Url::fromPath('icingadb/host/check-now', ['name' => $host->name]); } public static function scheduleCheck(Host $host): Url { return Url::fromPath('icingadb/host/schedule-check', ['name' => $host->name]); } public static function comments(Host $host): Url { return Url::fromPath('icingadb/comments', ['host.name' => $host->name]); } public static function downtimes(Host $host): Url { return Url::fromPath('icingadb/downtimes', ['host.name' => $host->name]); } public static function history(Host $host): Url { return Url::fromPath('icingadb/host/history', ['name' => $host->name]); } public static function removeAcknowledgement(Host $host): Url { return Url::fromPath('icingadb/host/remove-acknowledgement', ['name' => $host->name]); } public static function scheduleDowntime(Host $host): Url { return Url::fromPath('icingadb/host/schedule-downtime', ['name' => $host->name]); } public static function sendCustomNotification(Host $host): Url { return Url::fromPath('icingadb/host/send-custom-notification', ['name' => $host->name]); } public static function processCheckresult(Host $host): Url { return Url::fromPath('icingadb/host/process-checkresult', ['name' => $host->name]); } public static function toggleFeatures(Host $host): Url { return Url::fromPath('icingadb/host/toggle-features', ['name' => $host->name]); } public static function services(Host $host): Url { return Url::fromPath('icingadb/host/services', ['name' => $host->name]); } } icingadb-web-1.4.0/library/Icingadb/Common/HostStates.php000066400000000000000000000054061516124654200232500ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; /** * Collection of possible host states. */ class HostStates { public const UP = 0; public const DOWN = 1; public const PENDING = 99; /** * Get the integer value of the given textual host state * * @param string $state * * @return int * * @throws \InvalidArgumentException If the given host state is invalid, i.e. not known */ public static function int(string $state): int { switch (strtolower($state)) { case 'up': $int = self::UP; break; case 'down': $int = self::DOWN; break; case 'pending': $int = self::PENDING; break; default: throw new \InvalidArgumentException(sprintf('Invalid host state %d', $state)); } return $int; } /** * Get the textual representation of the passed host state * * @param ?int $state * * @return string * * @throws \InvalidArgumentException If the given host state is invalid, i.e. not known */ public static function text(?int $state = null): string { switch (true) { case $state === self::UP: $text = 'up'; break; case $state === self::DOWN: $text = 'down'; break; case $state === self::PENDING: $text = 'pending'; break; case $state === null: $text = 'not-available'; break; default: throw new \InvalidArgumentException(sprintf('Invalid host state %d', $state)); } return $text; } /** * Get the translated textual representation of the passed host state * * @param ?int $state * * @return string * * @throws \InvalidArgumentException If the given host state is invalid, i.e. not known */ public static function translated(?int $state = null): string { switch (true) { case $state === self::UP: $text = t('up'); break; case $state === self::DOWN: $text = t('down'); break; case $state === self::PENDING: $text = t('pending'); break; case $state === null: $text = t('not available'); break; default: throw new \InvalidArgumentException(sprintf('Invalid host state %d', $state)); } return $text; } } icingadb-web-1.4.0/library/Icingadb/Common/IcingaRedis.php000066400000000000000000000203231516124654200233230ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Exception; use Generator; use Icinga\Application\Config; use Icinga\Application\Logger; use Predis\Client as Redis; class IcingaRedis { public const DEFAULT_HOST = 'localhost'; public const DEFAULT_PORT = 6380; public const DEFAULT_DATABASE = 0; /** @var Redis Connection to the Icinga Redis */ private $redis; /** @var bool true if no connection attempt was successful */ private $redisUnavailable = false; /** * Get the singleton * * @deprecated Use {@see Backend::getRedis()} instead * @return static */ public static function instance(): self { return Backend::getRedis(); } /** * Get whether Redis is unavailable * * @return bool */ public function isUnavailable(): bool { if (! $this->redisUnavailable && $this->redis === null) { try { $this->getConnection(); } catch (Exception $_) { // getConnection already logs the error } } return $this->redisUnavailable; } /** * Get the connection to the Icinga Redis * * @return Redis * * @throws Exception */ public function getConnection(): Redis { if ($this->redisUnavailable) { throw new Exception('Redis is still not available'); } elseif ($this->redis === null) { try { $primaryRedis = $this->getPrimaryRedis(); } catch (Exception $e) { try { $secondaryRedis = $this->getSecondaryRedis(); } catch (Exception $ee) { $this->redisUnavailable = true; Logger::error($ee); throw $e; } if ($secondaryRedis === null) { $this->redisUnavailable = true; throw $e; } $this->redis = $secondaryRedis; return $this->redis; } $primaryTimestamp = $this->getLastIcingaHeartbeat($primaryRedis); if ($primaryTimestamp <= time() - 60) { $secondaryRedis = $this->getSecondaryRedis(); if ($secondaryRedis === null) { $this->redis = $primaryRedis; return $this->redis; } $secondaryTimestamp = $this->getLastIcingaHeartbeat($secondaryRedis); if ($secondaryTimestamp > $primaryTimestamp) { $this->redis = $secondaryRedis; } else { $this->redis = $primaryRedis; } } else { $this->redis = $primaryRedis; } } return $this->redis; } /** * Fetch host states * * @param array $ids The host ids to fetch results for * @param array $columns The columns to include in the results * * @return Generator */ public static function fetchHostState(array $ids, array $columns): Generator { return Backend::getRedis()->fetchState('icinga:host:state', $ids, $columns); } /** * Fetch service states * * @param array $ids The service ids to fetch results for * @param array $columns The columns to include in the results * * @return Generator */ public static function fetchServiceState(array $ids, array $columns): Generator { return Backend::getRedis()->fetchState('icinga:service:state', $ids, $columns); } /** * Fetch object states * * @param string $key The object key to access * @param array $ids The object ids to fetch results for * @param array $columns The columns to include in the results * * @return Generator */ protected function fetchState(string $key, array $ids, array $columns): Generator { try { $results = $this->getConnection()->hmget($key, $ids); } catch (Exception $_) { // The error has already been logged elsewhere return; } foreach ($results as $i => $json) { if ($json !== null) { $data = json_decode($json, true); $keyMap = array_fill_keys($columns, null); unset($keyMap['is_overdue']); // Is calculated by Icinga DB, not Icinga 2, hence it's never in redis yield $ids[$i] => array_intersect_key(array_merge($keyMap, $data), $keyMap); } } } /** * Get the last icinga heartbeat from redis * * @param ?Redis $redis * * @return float|int|null */ public static function getLastIcingaHeartbeat(?Redis $redis = null) { if ($redis === null) { $redis = Backend::getRedis()->getConnection(); } $stream = 'icinga:stats'; $rs = $redis->xread(1, null, [$stream], '0'); if (is_array($rs)) { $timestampKeyPos = array_search('timestamp', $rs[$stream][0][1], true); if ($timestampKeyPos !== false && isset($rs[$stream][0][1][$timestampKeyPos + 1])) { return $rs[$stream][0][1][$timestampKeyPos + 1] / 1000; } } return null; } /** * Get the primary redis instance * * @param ?Config $moduleConfig * @param ?Config $redisConfig * * @return Redis */ public static function getPrimaryRedis(?Config $moduleConfig = null, ?Config $redisConfig = null): Redis { if ($moduleConfig === null) { $moduleConfig = Config::module('icingadb'); } if ($redisConfig === null) { $redisConfig = Config::module('icingadb', 'redis'); } $section = $redisConfig->getSection('redis1'); $redis = new Redis([ 'host' => $section->get('host', static::DEFAULT_HOST), 'port' => $section->get('port', static::DEFAULT_PORT), 'database' => $section->get('database', static::DEFAULT_DATABASE), 'username' => $section->get('username'), 'password' => $section->get('password'), 'timeout' => 0.5 ] + self::getTlsParams($moduleConfig)); $redis->ping(); return $redis; } /** * Get the secondary redis instance if exists * * @param ?Config $moduleConfig * @param ?Config $redisConfig * * @return ?Redis */ public static function getSecondaryRedis(?Config $moduleConfig = null, ?Config $redisConfig = null) { if ($moduleConfig === null) { $moduleConfig = Config::module('icingadb'); } if ($redisConfig === null) { $redisConfig = Config::module('icingadb', 'redis'); } $section = $redisConfig->getSection('redis2'); $host = $section->host; if (empty($host)) { return null; } $redis = new Redis([ 'host' => $host, 'port' => $section->get('port', static::DEFAULT_PORT), 'database' => $section->get('database', static::DEFAULT_DATABASE), 'username' => $section->get('username'), 'password' => $section->get('password'), 'timeout' => 0.5 ] + self::getTlsParams($moduleConfig)); $redis->ping(); return $redis; } private static function getTlsParams(Config $config): array { $config = $config->getSection('redis'); if (! $config->get('tls', false)) { return []; } $ssl = []; if ($config->get('insecure')) { $ssl['verify_peer'] = false; $ssl['verify_peer_name'] = false; } else { $ca = $config->get('ca'); if ($ca !== null) { $ssl['cafile'] = $ca; } } $cert = $config->get('cert'); $key = $config->get('key'); if ($cert !== null && $key !== null) { $ssl['local_cert'] = $cert; $ssl['local_pk'] = $key; } return ['scheme' => 'tls', 'ssl' => $ssl]; } } icingadb-web-1.4.0/library/Icingadb/Common/Icons.php000066400000000000000000000014201516124654200222120ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; class Icons { public const COMMENT = 'comment'; public const HOST_DOWN = 'sitemap'; public const UNREACHABLE = 'sitemap'; public const IN_DOWNTIME = 'plug'; public const IS_ACKNOWLEDGED = 'check'; public const IS_FLAPPING = 'bolt'; public const IS_PERSISTENT = 'thumbtack'; public const NOTIFICATION = 'bell'; public const REMOVE = 'trash'; public const NOTIFICATIONS_DISABLED = 'bell-slash'; public const ACTIVE_CHECKS_DISABLED = 'eye-slash'; public const USER = 'user'; public const USERGROUP = 'users'; public const WARNING = 'exclamation-triangle'; } icingadb-web-1.4.0/library/Icingadb/Common/Links.php000066400000000000000000000075111516124654200222260ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Model\Downtime; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\NotificationHistory; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Model\Usergroup; use ipl\Web\Url; abstract class Links { public static function comment(Comment $comment): Url { return Url::fromPath('icingadb/comment', ['name' => $comment->name]); } public static function comments(): Url { return Url::fromPath('icingadb/comments'); } public static function commentsDelete(): Url { return Url::fromPath('icingadb/comments/delete'); } public static function commentsDetails(): Url { return Url::fromPath('icingadb/comments/details'); } public static function downtime(Downtime $downtime): Url { return Url::fromPath('icingadb/downtime', ['name' => $downtime->name]); } public static function downtimes(): Url { return Url::fromPath('icingadb/downtimes'); } public static function downtimesDelete(): Url { return Url::fromPath('icingadb/downtimes/delete'); } public static function downtimesDetails(): Url { return Url::fromPath('icingadb/downtimes/details'); } public static function host(Host $host): Url { return Url::fromPath('icingadb/host', ['name' => $host->name]); } public static function hostSource(Host $host): Url { return Url::fromPath('icingadb/host/source', ['name' => $host->name]); } public static function hostsDetails(): Url { return Url::fromPath('icingadb/hosts/details'); } public static function hostgroup($hostgroup): Url { return Url::fromPath('icingadb/hostgroup', ['name' => $hostgroup->name]); } public static function hosts(): Url { return Url::fromPath('icingadb/hosts'); } public static function service(Service $service, Host $host): Url { return Url::fromPath('icingadb/service', ['name' => $service->name, 'host.name' => $host->name]); } public static function serviceSource(Service $service, Host $host): Url { return Url::fromPath('icingadb/service/source', ['name' => $service->name, 'host.name' => $host->name]); } public static function servicesDetails(): Url { return Url::fromPath('icingadb/services/details'); } public static function servicegroup($servicegroup): Url { return Url::fromPath('icingadb/servicegroup', ['name' => $servicegroup->name]); } public static function services(): Url { return Url::fromPath('icingadb/services'); } public static function toggleHostsFeatures(): Url { return Url::fromPath('icingadb/hosts/toggle-features'); } public static function toggleServicesFeatures(): Url { return Url::fromPath('icingadb/services/toggle-features'); } public static function user(User $user): Url { return Url::fromPath('icingadb/contact', ['name' => $user->name]); } public static function usergroup(Usergroup $usergroup): Url { return Url::fromPath('icingadb/contactgroup', ['name' => $usergroup->name]); } public static function users(): Url { return Url::fromPath('icingadb/contacts'); } public static function usergroups(): Url { return Url::fromPath('icingadb/contactgroups'); } public static function event(History $event): Url { return Url::fromPath('icingadb/event', ['id' => bin2hex($event->id)]); } } icingadb-web-1.4.0/library/Icingadb/Common/LoadMore.php000066400000000000000000000047211516124654200226500ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Generator; use Icinga\Module\Icingadb\Widget\ItemList\PageSeparatorItem; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Orm\ResultSet; use ipl\Web\Url; trait LoadMore { /** @var int */ protected $pageSize; /** @var int */ protected $pageNumber; /** @var Url */ protected $loadMoreUrl; /** * Set the page size * * @param int $size * * @return $this */ public function setPageSize(int $size): self { $this->pageSize = $size; return $this; } /** * Set the page number * * @param int $number * * @return $this */ public function setPageNumber(int $number): self { $this->pageNumber = $number; return $this; } /** * Set the url to fetch more items * * @param Url $url * * @return $this */ public function setLoadMoreUrl(Url $url): self { $this->loadMoreUrl = $url; return $this; } /** * Iterate over the given data * * Add the page separator and the "LoadMore" button at the desired position * * @param ResultSet $result * * @return Generator */ protected function getIterator(ResultSet $result): Generator { $count = 0; $pageNumber = $this->pageNumber ?: 1; if ($pageNumber > 1) { $this->add(new PageSeparatorItem($pageNumber)); } foreach ($result as $data) { $count++; if ($count % $this->pageSize === 0) { $pageNumber++; } elseif ($count > $this->pageSize && $count % $this->pageSize === 1) { $this->add(new PageSeparatorItem($pageNumber)); } yield $data; } if ($count > 0 && $this->loadMoreUrl !== null) { $showMore = (new ShowMore( $result, $this->loadMoreUrl->setParam('page', $pageNumber) ->setAnchor('page-' . ($pageNumber)) )) ->setLabel(t('Load More')) ->setAttributes([ 'class' => 'load-more', 'data-no-icinga-ajax' => true ]); $this->add($showMore->setTag('li')->addAttributes(['class' => 'list-item'])); } } } icingadb-web-1.4.0/library/Icingadb/Common/Macros.php000066400000000000000000000103541516124654200223710ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use DateTime; use Icinga\Application\Logger; use Icinga\Module\Icingadb\Compat\CompatHost; use Icinga\Module\Icingadb\Compat\CompatService; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use ipl\Orm\Query; use ipl\Orm\ResultSet; use function ipl\Stdlib\get_php_type; trait Macros { /** * Get the given string with macros being resolved * * @param string $input The string in which to look for macros * @param Host|Service|CompatService|CompatHost $object The host or service used to resolve the macros * * @return string */ public function expandMacros(string $input, $object): string { if (preg_match_all('@\$([^\$\s]+)\$@', $input, $matches)) { foreach ($matches[1] as $key => $value) { $newValue = $this->resolveMacro($value, $object); if ($newValue !== $value) { $input = str_replace($matches[0][$key], $newValue, $input); } } } return $input; } /** * Resolve a macro based on the given object * * @param string $macro The macro to resolve * @param Host|Service|CompatService|CompatHost $object The host or service used to resolve the macros * * @return string */ public function resolveMacro(string $macro, $object): string { if ($object instanceof Host || (property_exists($object, 'type') && $object->type === 'host')) { $objectType = 'host'; } else { $objectType = 'service'; } $path = null; $macroType = $objectType; $isCustomVar = false; if (preg_match('/^((host|service)\.)?vars\.(.+)/', $macro, $matches)) { if (! empty($matches[2])) { $macroType = $matches[2]; } $path = $matches[3]; $isCustomVar = true; } elseif (preg_match('/^(\w+)\.(.+)/', $macro, $matches)) { $macroType = $matches[1]; $path = $matches[2]; } try { if ($path !== null) { if ($macroType !== $objectType) { $value = $object->$macroType; } else { $value = $object; } $properties = explode('.', $path); do { $column = array_shift($properties); if ($value instanceof Query || $value instanceof ResultSet || is_array($value)) { Logger::debug( 'Failed to resolve property "%s" on a "%s" type.', $isCustomVar ? 'vars' : $column, get_php_type($value) ); $value = null; break; } if ($isCustomVar) { $value = $value->vars[$path]; break; } $value = $value->$column; } while (! empty($properties) && $value !== null); } else { $value = $object->$macro; } } catch (\Exception $e) { $objectName = $object->name; if ($objectType === 'service' && isset($object->host)) { $objectName = $object->host->name . '!' . $objectName; } $value = null; Logger::debug('Unable to resolve macro "%s" on object "%s". An error occured: %s', $macro, $objectName, $e); } if ($value instanceof Query || $value instanceof ResultSet || is_array($value)) { Logger::debug( 'It is not allowed to use "%s" as a macro which produces a "%s" type as a result.', $macro, get_php_type($value) ); $value = null; } if ($value instanceof DateTime) { $value = $value->format(DateTime::ATOM); } elseif (is_bool($value)) { $value = $value ? 'y' : 'n'; } return (string) ($value ?? $macro); } } icingadb-web-1.4.0/library/Icingadb/Common/Model.php000066400000000000000000000011731516124654200222040ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use ipl\Orm\Query; use ipl\Sql\Connection; abstract class Model extends \ipl\Orm\Model { public static function on(Connection $db) { $query = parent::on($db); return $query->on( Query::ON_SELECT_ASSEMBLED, function () use ($query) { $auth = new class () { use Auth; }; $auth->assertColumnRestrictions($query->getFilter()); } ); } } icingadb-web-1.4.0/library/Icingadb/Common/ObjectInspectionDetail.php000066400000000000000000000264661516124654200255450ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use DateTime; use DateTimeZone; use Exception; use Icinga\Exception\IcingaException; use Icinga\Exception\Json\JsonDecodeException; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Widget\Detail\CustomVarTable; use Icinga\Util\Format; use Icinga\Util\Json; use InvalidArgumentException; use ipl\Html\BaseHtmlElement; use ipl\Html\FormattedString; use ipl\Html\HtmlElement; use ipl\Html\Table; use ipl\Html\Text; use ipl\Orm\Model; use ipl\Web\Widget\CopyToClipboard; use ipl\Web\Widget\EmptyState; abstract class ObjectInspectionDetail extends BaseHtmlElement { protected $tag = 'div'; protected $defaultAttributes = ['class' => ['object-detail', 'inspection-detail']]; /** @var Model */ protected $object; /** @var array */ protected $attrs; /** @var array */ protected $joins; public function __construct(Model $object, array $apiResult) { $this->object = $object; $this->attrs = $apiResult['attrs']; $this->joins = $apiResult['joins']; } /** * Render the object source location * * @return ?array */ protected function createSourceLocation() { if (! isset($this->attrs['source_location'])) { return; } return [ new HtmlElement('h2', null, Text::create(t('Source Location'))), FormattedString::create( t('You can find this object in %s on line %s.'), new HtmlElement('strong', null, Text::create($this->attrs['source_location']['path'])), new HtmlElement('strong', null, Text::create($this->attrs['source_location']['first_line'])) ) ]; } /** * Render object's last check result * * @return ?array */ protected function createLastCheckResult() { if (! isset($this->attrs['last_check_result'])) { return; } $command = $this->attrs['last_check_result']['command']; if (is_array($command)) { $command = join(' ', array_map('escapeshellarg', $command)); } $denylist = [ 'command', 'output', 'type', 'active' ]; if ($command) { $execCommand = new HtmlElement('pre', null, Text::create($command)); CopyToClipboard::attachTo($execCommand); } else { $execCommand = new EmptyState(t('n. a.')); } return [ new HtmlElement('h2', null, Text::create(t('Executed Command'))), $execCommand, new HtmlElement('h2', null, Text::create(t('Execution Details'))), $this->createNameValueTable( array_diff_key($this->attrs['last_check_result'], array_flip($denylist)), [ 'execution_end' => [$this, 'formatTimestamp'], 'execution_start' => [$this, 'formatTimestamp'], 'schedule_end' => [$this, 'formatTimestamp'], 'schedule_start' => [$this, 'formatTimestamp'], 'ttl' => [$this, 'formatSeconds'], 'state' => [$this, 'formatState'] ] ) ]; } protected function createRedisInfo(): array { $title = new HtmlElement('h2', null, Text::create(t('Volatile State Details'))); try { $json = Backend::getRedis()->getConnection() ->hGet("icinga:{$this->object->getTableName()}:state", bin2hex($this->object->id)); } catch (Exception $e) { return [$title, sprintf('Failed to load redis data: %s', $e->getMessage())]; } if (! $json) { return [$title, new EmptyState(t('No data available in redis'))]; } try { $data = Json::decode($json, true); } catch (JsonDecodeException $e) { return [$title, sprintf('Failed to decode redis data: %s', $e->getMessage())]; } $denylist = [ 'commandline', 'environment_id', 'id' ]; return [$title, $this->createNameValueTable( array_diff_key($data, array_flip($denylist)), [ 'last_state_change' => [$this, 'formatMillisecondTimestamp'], 'last_update' => [$this, 'formatMillisecondTimestamp'], 'next_check' => [$this, 'formatMillisecondTimestamp'], 'next_update' => [$this, 'formatMillisecondTimestamp'], 'check_timeout' => [$this, 'formatMilliseconds'], 'execution_time' => [$this, 'formatMilliseconds'], 'latency' => [$this, 'formatMilliseconds'], 'hard_state' => [$this, 'formatState'], 'previous_soft_state' => [$this, 'formatState'], 'previous_hard_state' => [$this, 'formatState'], 'state' => [$this, 'formatState'] ] )]; } protected function createAttributes(): array { $denylist = [ 'name', '__name', 'host_name', 'display_name', 'last_check_result', 'source_location', 'package', 'version', 'type', 'active', 'paused', 'ha_mode' ]; return [ new HtmlElement('h2', null, Text::create(t('Object Attributes'))), $this->createNameValueTable( array_diff_key($this->attrs, array_flip($denylist)), [ 'acknowledgement_expiry' => [$this, 'formatTimestamp'], 'acknowledgement_last_change' => [$this, 'formatTimestamp'], 'check_timeout' => [$this, 'formatSeconds'], 'flapping_last_change' => [$this, 'formatTimestamp'], 'last_check' => [$this, 'formatTimestamp'], 'last_hard_state_change' => [$this, 'formatTimestamp'], 'last_state_change' => [$this, 'formatTimestamp'], 'last_state_ok' => [$this, 'formatTimestamp'], 'last_state_up' => [$this, 'formatTimestamp'], 'last_state_warning' => [$this, 'formatTimestamp'], 'last_state_critical' => [$this, 'formatTimestamp'], 'last_state_down' => [$this, 'formatTimestamp'], 'last_state_unknown' => [$this, 'formatTimestamp'], 'last_state_unreachable' => [$this, 'formatTimestamp'], 'next_check' => [$this, 'formatTimestamp'], 'next_update' => [$this, 'formatTimestamp'], 'previous_state_change' => [$this, 'formatTimestamp'], 'check_interval' => [$this, 'formatSeconds'], 'retry_interval' => [$this, 'formatSeconds'], 'last_hard_state' => [$this, 'formatState'], 'last_state' => [$this, 'formatState'], 'state' => [$this, 'formatState'] ] ) ]; } protected function createCustomVariables() { $query = $this->object->customvar ->columns(['name', 'value']); $result = []; foreach ($query as $row) { $result[$row->name] = json_decode($row->value, true) ?? $row->value; } if (! empty($result)) { $vars = new CustomVarTable($result); } else { $vars = new EmptyState(t('No custom variables configured.')); } return [ new HtmlElement('h2', null, Text::create(t('Custom Variables'))), $vars ]; } /** * Format the given value as a json * * @param mixed $json * * @return BaseHtmlElement|string */ private function formatJson($json) { if (is_scalar($json)) { return Json::encode($json, JSON_UNESCAPED_SLASHES); } return new HtmlElement( 'pre', null, Text::create(Json::encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) ); } /** * Format the given timestamp * * @param int|float|null $ts * * @return EmptyState|string */ private function formatTimestamp($ts) { if (empty($ts)) { return new EmptyState(t('n. a.')); } if (is_float($ts)) { $dt = DateTime::createFromFormat('U.u', sprintf('%F', $ts)); } else { $dt = (new DateTime())->setTimestamp($ts); } return $dt->setTimezone(new DateTimeZone('UTC')) ->format('Y-m-d\TH:i:s.vP'); } /** * Format the given timestamp (in milliseconds) * * @param int|float|null $ms * * @return EmptyState|string */ private function formatMillisecondTimestamp($ms) { return $this->formatTimestamp($ms / 1000.0); } private function formatSeconds($s): string { return Format::seconds($s); } private function formatMilliseconds($ms): string { return Format::seconds($ms / 1000.0); } private function formatState(int $state) { try { switch (true) { case $this->object instanceof Host: return HostStates::text($state); case $this->object instanceof Service: return ServiceStates::text($state); default: return $state; } } catch (InvalidArgumentException $_) { // The Icinga 2 API sometimes delivers strange details return (string) $state; } } private function createNameValueTable(array $data, array $formatters): Table { $table = new Table(); $table->addAttributes(['class' => 'name-value-table']); foreach ($data as $name => $value) { if (empty($value) && ($value === null || is_string($value) || is_array($value))) { $value = new EmptyState(t('n. a.')); } else { try { if (isset($formatters[$name])) { $value = call_user_func($formatters[$name], $value); } else { $value = $this->formatJson($value); if ($value instanceof BaseHtmlElement) { CopyToClipboard::attachTo($value); } } } catch (Exception $e) { $value = new EmptyState(IcingaException::describe($e)); } } $table->addHtml(Table::tr([ Table::th($name), Table::td($value) ])); } return $table; } } icingadb-web-1.4.0/library/Icingadb/Common/SearchControls.php000066400000000000000000000050401516124654200240720ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Icinga\Module\Icingadb\Web\Control\SearchBar\ObjectSuggestions; use ipl\Html\Html; use ipl\Orm\Query; use ipl\Web\Control\SearchBar; use ipl\Web\Url; use ipl\Web\Widget\ContinueWith; trait SearchControls { use \ipl\Web\Compat\SearchControls { \ipl\Web\Compat\SearchControls::createSearchBar as private webCreateSearchBar; } public function fetchFilterColumns(Query $query): array { return iterator_to_array(ObjectSuggestions::collectFilterColumns($query->getModel(), $query->getResolver())); } /** * Create and return the SearchBar * * @param Query $query The query being filtered * @param Url $redirectUrl Url to redirect to upon success * @param array $preserveParams Query params to preserve when redirecting * * @return SearchBar */ public function createSearchBar(Query $query, ...$params): SearchBar { $searchBar = $this->webCreateSearchBar($query, ...$params); if (($wrapper = $searchBar->getWrapper()) && ! $wrapper->getWrapper()) { // TODO: Remove this once ipl-web v0.7.0 is required $searchBar->addWrapper(Html::tag('div', ['class' => 'search-controls'])); } return $searchBar; } /** * Create and return a ContinueWith * * This will automatically be appended to the SearchBar's wrapper. It's not necessary * to add it separately as control or content! * * @param Url $detailsUrl * @param SearchBar $searchBar * @param bool $hasResults Whether the current query has results * * @return ContinueWith */ public function createContinueWith(Url $detailsUrl, SearchBar $searchBar, bool $hasResults = true): ContinueWith { if ($hasResults) { $continueWith = ContinueWith::create( $detailsUrl, [$searchBar, 'getFilter'], t('Show bulk processing actions for all filtered results'), t('A filter is required to show bulk processing actions'), ); $continueWith->setBaseTarget('_next'); } else { $continueWith = ContinueWith::createDisabled(t('No items found')); } $continueWith->getAttributes() ->set('id', $this->getRequest()->protectId('continue-with')); $searchBar->getWrapper()->add($continueWith); return $continueWith; } } icingadb-web-1.4.0/library/Icingadb/Common/ServiceLink.php000066400000000000000000000023101516124654200233540ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use ipl\Html\FormattedString; use ipl\Html\Html; use ipl\Web\Widget\StateBall; trait ServiceLink { protected function createServiceLink(Service $service, Host $host, bool $withStateBall = false): FormattedString { $content = []; if ($withStateBall) { $content[] = new StateBall($service->state->getStateText(), StateBall::SIZE_MEDIUM); $content[] = ' '; } $content[] = $service->display_name; return Html::sprintf( t('%s on %s', ' on '), Html::tag('a', ['href' => Links::service($service, $host), 'class' => 'subject'], $content), Html::tag( 'a', ['href' => Links::host($host), 'class' => 'subject'], [ new StateBall($host->state->getStateText(), StateBall::SIZE_MEDIUM), ' ', $host->display_name ] ) ); } } icingadb-web-1.4.0/library/Icingadb/Common/ServiceLinks.php000066400000000000000000000062211516124654200235440ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use ipl\Web\Url; abstract class ServiceLinks { public static function acknowledge(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/service/acknowledge', ['name' => $service->name, 'host.name' => $host->name] ); } public static function addComment(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/service/add-comment', ['name' => $service->name, 'host.name' => $host->name] ); } public static function checkNow(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/service/check-now', ['name' => $service->name, 'host.name' => $host->name] ); } public static function scheduleCheck(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/service/schedule-check', ['name' => $service->name, 'host.name' => $host->name] ); } public static function comments(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/comments', ['service.name' => $service->name, 'host.name' => $host->name] ); } public static function downtimes(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/downtimes', ['service.name' => $service->name, 'host.name' => $host->name] ); } public static function history(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/service/history', ['name' => $service->name, 'host.name' => $host->name] ); } public static function removeAcknowledgement(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/service/remove-acknowledgement', ['name' => $service->name, 'host.name' => $host->name] ); } public static function scheduleDowntime(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/service/schedule-downtime', ['name' => $service->name, 'host.name' => $host->name] ); } public static function sendCustomNotification(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/service/send-custom-notification', ['name' => $service->name, 'host.name' => $host->name] ); } public static function processCheckresult(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/service/process-checkresult', ['name' => $service->name, 'host.name' => $host->name] ); } public static function toggleFeatures(Service $service, Host $host): Url { return Url::fromPath( 'icingadb/service/toggle-features', ['name' => $service->name, 'host.name' => $host->name] ); } } icingadb-web-1.4.0/library/Icingadb/Common/ServiceStates.php000066400000000000000000000067151516124654200237370ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; /** * Collection of possible service states. */ class ServiceStates { public const OK = 0; public const WARNING = 1; public const CRITICAL = 2; public const UNKNOWN = 3; public const PENDING = 99; /** * Get the integer value of the given textual service state * * @param string $state * * @return int * * @throws \InvalidArgumentException If the given service state is invalid, i.e. not known */ public static function int(string $state): int { switch (strtolower($state)) { case 'ok': $int = self::OK; break; case 'warning': $int = self::WARNING; break; case 'critical': $int = self::CRITICAL; break; case 'unknown': $int = self::UNKNOWN; break; case 'pending': $int = self::PENDING; break; default: throw new \InvalidArgumentException(sprintf('Invalid service state %d', $state)); } return $int; } /** * Get the textual representation of the passed service state * * @param ?int $state * * @return string * * @throws \InvalidArgumentException If the given service state is invalid, i.e. not known */ public static function text(?int $state = null): string { switch (true) { case $state === self::OK: $text = 'ok'; break; case $state === self::WARNING: $text = 'warning'; break; case $state === self::CRITICAL: $text = 'critical'; break; case $state === self::UNKNOWN: $text = 'unknown'; break; case $state === self::PENDING: $text = 'pending'; break; case $state === null: $text = 'not-available'; break; default: throw new \InvalidArgumentException(sprintf('Invalid service state %d', $state)); } return $text; } /** * Get the translated textual representation of the passed service state * * @param ?int $state * * @return string * * @throws \InvalidArgumentException If the given service state is invalid, i.e. not known */ public static function translated(?int $state = null): string { switch (true) { case $state === self::OK: $text = t('ok'); break; case $state === self::WARNING: $text = t('warning'); break; case $state === self::CRITICAL: $text = t('critical'); break; case $state === self::UNKNOWN: $text = t('unknown'); break; case $state === self::PENDING: $text = t('pending'); break; case $state === null: $text = t('not available'); break; default: throw new \InvalidArgumentException(sprintf('Invalid service state %d', $state)); } return $text; } } icingadb-web-1.4.0/library/Icingadb/Common/StateBadges.php000066400000000000000000000126101516124654200233300ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use InvalidArgumentException; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Html\HtmlElement; use ipl\Stdlib\BaseFilter; use ipl\Stdlib\Filter; use ipl\Web\Url; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBadge; abstract class StateBadges extends BaseHtmlElement { use BaseFilter; /** @var object $item */ protected $item; /** @var string */ protected $type; /** @var string Prefix */ protected $prefix; /** @var ?Url Badge link */ protected $url; protected $tag = 'ul'; protected $defaultAttributes = ['class' => 'state-badges']; /** * Create a new widget for state badges * * @param object $item */ public function __construct($item) { $this->item = $item; $this->type = $this->getType(); $this->prefix = $this->getPrefix(); $this->url = $this->getBaseUrl(); } /** * Get the type of the items * * @return string */ abstract protected function getType(): string; /** * Get the prefix for accessing state information * * @return string */ abstract protected function getPrefix(): string; /** * Get the badge base URL * * @return ?Url */ protected function getBaseUrl(): ?Url { return null; } /** * Get the integer of the given state text * * @param string $state * * @return int * * @throws InvalidArgumentException if the given state is not valid */ protected function getStateInt(string $state): int { throw new InvalidArgumentException(sprintf('%s is not a valid state', $state)); } /** * Get the badge URL * * @return ?Url */ public function getUrl(): ?Url { return $this->url; } /** * Set the badge URL * * @param Url $url * * @return $this */ public function setUrl(Url $url): self { $this->url = $url; return $this; } /** * Create a badge link * * @param mixed $content * @param ?Filter\Rule $filter * * @return Link */ protected function createLink($content, ?Filter\Rule $filter = null): Link { $url = clone $this->getUrl(); $urlFilter = Filter::all(); if ($filter !== null) { $urlFilter->add($filter); } if ($this->hasBaseFilter()) { $urlFilter->add($this->getBaseFilter()); } if (! $urlFilter->isEmpty()) { $url->setFilter($urlFilter); } return new Link($content, $url); } /** * Create a state bade * * @param string $state * * @return ?BaseHtmlElement */ protected function createBadge(string $state): ?BaseHtmlElement { $key = $this->prefix . "_{$state}"; if (empty($this->item->$key)) { return null; } $stateBadge = new StateBadge($this->item->$key, $state); if ($this->url !== null) { $stateBadge = $this->createLink( $stateBadge, Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)) ); } return new HtmlElement('li', null, $stateBadge); } /** * Create a state group * * @param string $state * * @return ?BaseHtmlElement */ protected function createGroup(string $state): ?BaseHtmlElement { $content = []; $handledKey = $this->prefix . "_{$state}_handled"; $unhandledKey = $this->prefix . "_{$state}_unhandled"; if (isset($this->item->$unhandledKey) && $this->item->$unhandledKey) { $unhandledStateBadge = new StateBadge($this->item->$unhandledKey, $state); if ($this->url !== null) { $unhandledStateBadge = $this->createLink( $unhandledStateBadge, Filter::all( Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)), Filter::equal($this->type . '.state.is_handled', 'n'), Filter::equal($this->type . '.state.is_reachable', 'y') ) ); } $content[] = new HtmlElement('li', null, $unhandledStateBadge); } if (isset($this->item->$handledKey) && $this->item->$handledKey) { $handledStateBadge = new StateBadge($this->item->$handledKey, $state, true); if ($this->url !== null) { $handledStateBadge = $this->createLink( $handledStateBadge, Filter::all( Filter::equal($this->type . '.state.soft_state', $this->getStateInt($state)), Filter::any( Filter::equal($this->type . '.state.is_handled', 'y'), Filter::equal($this->type . '.state.is_reachable', 'n') ) ) ); } $content[] = new HtmlElement('li', null, $handledStateBadge); } if (empty($content)) { return null; } return Html::tag('li', Html::tag('ul', $content)); } } icingadb-web-1.4.0/library/Icingadb/Common/TicketLinks.php000066400000000000000000000023251516124654200233700ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Common; use Icinga\Application\Hook; use Icinga\Application\Hook\TicketHook; trait TicketLinks { /** @var bool Whether the ticket link is disabled */ protected $ticketLinkDisabled = false; /** * Set whether the ticket link is disabled * * @param bool $state * * @return $this */ public function setTicketLinkDisabled(bool $state = true): self { $this->ticketLinkDisabled = $state; return $this; } /** * Get whether the ticket link is disabled * * @return bool */ public function isTicketLinkDisabled(): bool { return $this->ticketLinkDisabled; } /** * Get whether list items should render host and service links * * @return string */ public function createTicketLinks($text): string { if ($this->isTicketLinkDisabled() || ! Hook::has('ticket')) { return $text ?? ''; } /** @var TicketHook $tickets */ $tickets = Hook::first('ticket'); return $tickets->createLinks($text); } } icingadb-web-1.4.0/library/Icingadb/Compat/000077500000000000000000000000001516124654200204245ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Compat/CompatHost.php000066400000000000000000000117551516124654200232270ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Compat; use Icinga\Module\Monitoring\Object\Host; class CompatHost extends Host { use CompatObject; private $legacyColumns = [ 'host_action_url' => ['path' => ['action_url', 'action_url']], 'action_url' => ['path' => ['action_url', 'action_url']], 'host_address' => ['path' => ['address']], 'host_address6' => ['path' => ['address6']], 'host_alias' => ['path' => ['display_name']], 'host_check_interval' => ['path' => ['check_interval']], 'host_display_name' => ['path' => ['display_name']], 'host_icon_image' => ['path' => ['icon_image', 'icon_image']], 'host_icon_image_alt' => ['path' => ['icon_image_alt']], 'host_name' => ['path' => ['name']], 'host_notes' => ['path' => ['notes']], 'host_notes_url' => ['path' => ['notes_url', 'notes_url']], 'host_acknowledged' => [ 'path' => ['state', 'is_acknowledged'], 'type' => 'bool' ], 'host_acknowledgement_type' => [ 'path' => ['state', 'is_acknowledged'], 'type' => 'bool' ], 'host_active_checks_enabled' => [ 'path' => ['active_checks_enabled'], 'type' => 'bool' ], 'host_active_checks_enabled_changed' => null, 'host_attempt' => null, 'host_check_command' => ['path' => ['checkcommand_name']], 'host_check_execution_time' => ['path' => ['state', 'execution_time']], 'host_check_latency' => ['path' => ['state', 'latency']], 'host_check_source' => ['path' => ['state', 'check_source']], 'host_check_timeperiod' => ['path' => ['check_timeperiod_name']], 'host_current_check_attempt' => ['path' => ['state', 'check_attempt']], 'host_current_notification_number' => null, 'host_event_handler_enabled' => [ 'path' => ['event_handler_enabled'], 'type' => 'bool' ], 'host_event_handler_enabled_changed' => null, 'host_flap_detection_enabled' => [ 'path' => ['flapping_enabled'], 'type' => 'bool' ], 'host_flap_detection_enabled_changed' => null, 'host_handled' => [ 'path' => ['state', 'is_handled'], 'type' => 'bool' ], 'host_in_downtime' => [ 'path' => ['state', 'in_downtime'], 'type' => 'bool' ], 'host_is_flapping' => [ 'path' => ['state', 'is_flapping'], 'type' => 'bool' ], 'host_is_reachable' => [ 'path' => ['state', 'is_reachable'], 'type' => 'bool' ], 'host_last_check' => ['path' => ['state', 'last_update']], 'host_last_notification' => null, 'host_last_state_change' => ['path' => ['state', 'last_state_change']], 'host_long_output' => ['path' => ['state', 'long_output']], 'host_max_check_attempts' => ['path' => ['max_check_attempts']], 'host_next_check' => ['path' => ['state', 'next_check']], 'host_next_update' => ['path' => ['state', 'next_update']], 'host_notifications_enabled' => [ 'path' => ['notifications_enabled'], 'type' => 'bool' ], 'host_notifications_enabled_changed' => null, 'host_obsessing' => null, 'host_obsessing_changed' => null, 'host_output' => ['path' => ['state', 'output']], 'host_passive_checks_enabled' => [ 'path' => ['passive_checks_enabled'], 'type' => 'bool' ], 'host_passive_checks_enabled_changed' => null, 'host_percent_state_change' => null, 'host_perfdata' => ['path' => ['state', 'performance_data']], 'host_process_perfdata' => [ 'path' => ['perfdata_enabled'], 'type' => 'bool' ], 'host_state' => ['path' => ['state', 'soft_state']], 'host_state_type' => ['path' => ['state', 'state_type']], 'instance_name' => null ]; } icingadb-web-1.4.0/library/Icingadb/Compat/CompatObject.php000066400000000000000000000237451516124654200235220ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Compat; use Icinga\Exception\NotImplementedError; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Model\CustomvarFlat; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\Servicegroup; use Icinga\Module\Icingadb\Model\ServicestateSummary; use InvalidArgumentException; use ipl\Orm\Model; use ipl\Orm\Query; use ipl\Stdlib\Filter; use LogicException; use function ipl\Stdlib\get_php_type; trait CompatObject { use Auth; use Database; /** @var array Non-obscured custom variables */ protected $rawCustomvars; /** @var array Non-obscured host custom variables */ protected $rawHostCustomvars; /** @var Model $object */ private $object; public function __construct(Model $object) { $this->object = $object; } public static function fromModel(Model $object) { switch (true) { case $object instanceof Host: return new CompatHost($object); case $object instanceof Service: return new CompatService($object); default: throw new InvalidArgumentException(sprintf( 'Host or Service Model instance expected, got "%s" instead.', get_php_type($object) )); } } /** * Get this object's name * * @return string */ public function getName(): string { return $this->object->name ?? ''; } public function fetch(): bool { return true; } protected function fetchRawCustomvars(): self { if ($this->rawCustomvars !== null) { return $this; } $vars = $this->object->customvar->execute(); $customVars = []; foreach ($vars as $row) { $customVars[$row->name] = $row->value; } $this->rawCustomvars = $customVars; return $this; } protected function fetchRawHostCustomvars(): self { if ($this->rawHostCustomvars !== null) { return $this; } $vars = $this->object->host->customvar->execute(); $customVars = []; foreach ($vars as $row) { $customVars[$row->name] = $row->value; } $this->rawHostCustomvars = $customVars; return $this; } public function fetchComments() { $this->comments = []; return $this; } public function fetchContactgroups() { $this->contactgroups = []; return $this; } public function fetchContacts() { $this->contacts = []; return $this; } public function fetchCustomvars(): self { if ($this->customvars !== null) { return $this; } $this->customvars = (new CustomvarFlat())->unFlattenVars($this->object->customvar_flat); return $this; } public function fetchHostVariables() { if (isset($this->hostVariables)) { return $this; } $this->hostVariables = []; foreach ($this->object->customvar as $customvar) { $this->hostVariables[strtolower($customvar->name)] = json_decode($customvar->value); } return $this; } public function fetchServiceVariables() { if (isset($this->serviceVariables)) { return $this; } $this->serviceVariables = []; foreach ($this->object->customvar as $customvar) { $this->serviceVariables[strtolower($customvar->name)] = json_decode($customvar->value); } return $this; } public function fetchDowntimes() { $this->downtimes = []; return $this; } public function fetchEventhistory() { $this->eventhistory = []; return $this; } public function fetchHostgroups() { if ($this->type === self::TYPE_HOST) { $hostname = $this->object->name; $hostgroupQuery = clone $this->object->hostgroup; } else { $hostname = $this->object->host->name; $hostgroupQuery = clone $this->object->host->hostgroup; } $hostgroupQuery ->columns(['name', 'display_name']) ->filter(Filter::equal('host.name', $hostname)); /** @var Query $hostgroupQuery */ $this->hostgroups = []; foreach ($hostgroupQuery as $hostgroup) { $this->hostgroups[$hostgroup->name] = $hostgroup->display_name; } return $this; } public function fetchServicegroups() { if ($this->type === self::TYPE_HOST) { $hostname = $this->object->name; $query = Servicegroup::on($this->getDb()); } else { $hostname = $this->object->host->name; $query = (clone $this->object->servicegroup); } $query ->columns(['name', 'display_name']) ->filter(Filter::equal('host.name', $hostname)); if ($this->type === self::TYPE_SERVICE) { $query->filter(Filter::equal('service.name', $this->object->name)); } $this->servicegroups = []; foreach ($query as $serviceGroup) { $this->servicegroups[$serviceGroup->name] = $serviceGroup->display_name; } return $this; } public function fetchStats() { $query = ServicestateSummary::on($this->getDb()); if ($this->type === self::TYPE_HOST) { $query->filter(Filter::equal('host.name', $this->object->name)); } else { $query->filter(Filter::all( Filter::equal('host.name', $this->object->host->name), Filter::equal('service.name', $this->object->name) )); } $result = $query->first(); $this->stats = (object) [ 'services_total' => $result->services_total, 'services_ok' => $result->services_ok, 'services_critical' => $result->services_critical_handled + $result->services_critical_unhandled, 'services_critical_unhandled' => $result->services_critical_unhandled, 'services_critical_handled' => $result->services_critical_handled, 'services_warning' => $result->services_warning_handled + $result->services_warning_unhandled, 'services_warning_unhandled' => $result->services_warning_unhandled, 'services_warning_handled' => $result->services_warning_handled, 'services_unknown' => $result->services_unknown_handled + $result->services_unknown_unhandled, 'services_unknown_unhandled' => $result->services_unknown_unhandled, 'services_unknown_handled' => $result->services_unknown_handled, 'services_pending' => $result->services_pending ]; return $this; } public function __get($name) { if (property_exists($this, $name)) { if ($this->$name === null) { $fetchMethod = 'fetch' . ucfirst($name); $this->$fetchMethod(); } return $this->$name; } if (preg_match('/^_(host|service)_(.+)/i', $name, $matches)) { switch (strtolower($matches[1])) { case $this->type: $customvars = $this->fetchRawCustomvars()->rawCustomvars; break; case self::TYPE_HOST: $customvars = $this->fetchRawHostCustomvars()->rawHostCustomvars; break; case self::TYPE_SERVICE: throw new LogicException('Cannot fetch service custom variables for non-service objects'); } $variableName = strtolower($matches[2]); if (isset($customvars[$variableName])) { return $customvars[$variableName]; } return null; // Unknown custom variables MUST NOT throw an error } if (! array_key_exists($name, $this->legacyColumns) && ! $this->object->hasProperty($name)) { if (isset($this->customvars[$name])) { return $this->customvars[$name]; } if (substr($name, 0, strlen($this->prefix)) !== $this->prefix) { $name = $this->prefix . $name; } } if (array_key_exists($name, $this->legacyColumns)) { $opts = $this->legacyColumns[$name]; if ($opts === null) { return null; } $path = $opts['path']; $value = null; if (! empty($path)) { $value = $this->object; do { $col = array_shift($path); $value = $value->$col; } while (! empty($path) && $value !== null); } if (isset($opts['type'])) { $method = 'get' . ucfirst($opts['type']) . 'Type'; $value = $this->$method($value); } return $value; } return $this->object->$name; } public function __isset($name) { if (property_exists($this, $name)) { return isset($this->$name); } if (isset($this->legacyColumns[$name]) || isset($this->object->$name)) { return true; } return false; } /** * @throws NotImplementedError Don't use! */ protected function getDataView() { throw new NotImplementedError('getDataView() is not supported'); } /** * Get the bool type of the given value as an int * * @param bool|string $value * * @return ?int */ private function getBoolType($value) { if ($value === 'sticky') { return 2; } if (is_string($value)) { return null; } return (int) $value; } } icingadb-web-1.4.0/library/Icingadb/Compat/CompatService.php000066400000000000000000000162371516124654200237120ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Compat; use Icinga\Module\Monitoring\Object\Service; class CompatService extends Service { use CompatObject; private $legacyColumns = [ 'instance_name' => null, 'host_attempt' => null, 'host_icon_image' => ['path' => ['host', 'icon_image', 'icon_image']], 'host_icon_image_alt' => ['path' => ['host', 'icon_image_alt']], 'host_acknowledged' => [ 'path' => ['host', 'state', 'is_acknowledged'], 'type' => 'bool' ], 'host_active_checks_enabled' => [ 'path' => ['host', 'active_checks_enabled'], 'type' => 'bool' ], 'host_address' => ['path' => ['host', 'address']], 'host_address6' => ['path' => ['host', 'address6']], 'host_alias' => ['path' => ['host', 'display_name']], 'host_display_name' => ['path' => ['host', 'display_name']], 'host_handled' => [ 'path' => ['host', 'state', 'is_handled'], 'type' => 'bool' ], 'host_in_downtime' => [ 'path' => ['host', 'state', 'in_downtime'], 'type' => 'bool' ], 'host_is_flapping' => [ 'path' => ['host', 'state', 'is_flapping'], 'type' => 'bool' ], 'host_last_state_change' => ['path' => ['host', 'state', 'last_state_change']], 'host_name' => ['path' => ['host', 'name']], 'host_notifications_enabled' => [ 'path' => ['host', 'notifications_enabled'], 'type' => 'bool' ], 'host_passive_checks_enabled' => [ 'path' => ['host', 'passive_checks_enabled'], 'type' => 'bool' ], 'host_state' => ['path' => ['host', 'state', 'soft_state']], 'host_state_type' => ['path' => ['host', 'state', 'state_type']], 'service_icon_image' => ['path' => ['icon_image', 'icon_image']], 'service_icon_image_alt' => ['path' => ['icon_image_alt']], 'service_acknowledged' => [ 'path' => ['state', 'is_acknowledged'], 'type' => 'bool' ], 'service_acknowledgement_type' => [ 'path' => ['state', 'is_acknowledged'], 'type' => 'bool' ], 'service_action_url' => ['path' => ['action_url', 'action_url']], 'action_url' => ['path' => ['action_url', 'action_url']], 'service_active_checks_enabled' => [ 'path' => ['active_checks_enabled'], 'type' => 'bool' ], 'service_active_checks_enabled_changed' => null, 'service_attempt' => null, 'service_check_command' => ['path' => ['checkcommand_name']], 'service_check_execution_time' => ['path' => ['state', 'execution_time']], 'service_check_interval' => ['path' => ['check_interval']], 'service_check_latency' => ['path' => ['state', 'latency']], 'service_check_source' => ['path' => ['state', 'check_source']], 'service_check_timeperiod' => ['path' => ['check_timeperiod_name']], 'service_current_notification_number' => null, 'service_description' => ['path' => ['name']], 'service_display_name' => ['path' => ['display_name']], 'service_event_handler_enabled' => [ 'path' => ['event_handler_enabled'], 'type' => 'bool' ], 'service_event_handler_enabled_changed' => null, 'service_flap_detection_enabled' => [ 'path' => ['flapping_enabled'], 'type' => 'bool' ], 'service_flap_detection_enabled_changed' => null, 'service_handled' => [ 'path' => ['state', 'is_handled'], 'type' => 'bool' ], 'service_in_downtime' => [ 'path' => ['state', 'in_downtime'], 'type' => 'bool' ], 'service_is_flapping' => [ 'path' => ['state', 'is_flapping'], 'type' => 'bool' ], 'service_is_reachable' => [ 'path' => ['state', 'is_reachable'], 'type' => 'bool' ], 'service_last_check' => ['path' => ['state', 'last_update']], 'service_last_notification' => null, 'service_last_state_change' => ['path' => ['state', 'last_state_change']], 'service_long_output' => ['path' => ['state', 'long_output']], 'service_next_check' => ['path' => ['state', 'next_check']], 'service_next_update' => ['path' => ['state', 'next_update']], 'service_notes' => ['path' => ['notes']], 'service_notes_url' => ['path' => ['notes_url', 'notes_url']], 'service_notifications_enabled' => [ 'path' => ['notifications_enabled'], 'type' => 'bool' ], 'service_notifications_enabled_changed' => null, 'service_obsessing' => null, 'service_obsessing_changed' => null, 'service_output' => ['path' => ['state', 'output']], 'service_passive_checks_enabled' => [ 'path' => ['passive_checks_enabled'], 'type' => 'bool' ], 'service_passive_checks_enabled_changed' => null, 'service_percent_state_change' => null, 'service_perfdata' => ['path' => ['state', 'performance_data']], 'service_process_perfdata' => [ 'path' => ['perfdata_enabled'], 'type' => 'bool' ], 'service_state' => ['path' => ['state', 'soft_state']], 'service_state_type' => ['path' => ['state', 'state_type']] ]; /** * Get this service's host * * @return CompatHost */ public function getHost(): CompatHost { if ($this->host === null) { $this->host = new CompatHost($this->object->host); } return $this->host; } protected function fetchHost() { $this->getHost(); } } icingadb-web-1.4.0/library/Icingadb/Compat/UrlMigrator.php000066400000000000000000001455231516124654200234160ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Compat; use Icinga\Web\UrlParams; use InvalidArgumentException; use ipl\Stdlib\Filter; use ipl\Web\Filter\QueryString; use ipl\Web\Url; class UrlMigrator { public const NO_YES = ['n', 'y']; public const USE_EXPR = 'use-expr'; public const SORT_ONLY = 'sort-only'; public const LOWER_EXPR = 'lower-expr'; public const DROP = 'drop'; public const SUPPORTED_PATHS = [ 'monitoring/list/hosts' => ['hosts', 'icingadb/hosts'], 'monitoring/hosts/show' => ['multipleHosts', 'icingadb/hosts/details'], 'monitoring/host/show' => ['host', 'icingadb/host'], 'monitoring/host/services' => ['host', 'icingadb/host/services'], 'monitoring/host/history' => ['host', 'icingadb/host/history'], 'monitoring/list/services' => ['services', 'icingadb/services'], 'monitoring/list/servicegrid' => ['servicegrid', 'icingadb/services/grid'], 'monitoring/services/show' => ['multipleServices', 'icingadb/services/details'], 'monitoring/service/show' => ['service', 'icingadb/service'], 'monitoring/service/history' => ['service', 'icingadb/service/history'], 'monitoring/list/hostgroups' => ['hostgroups', 'icingadb/hostgroups'], 'monitoring/list/servicegroups' => ['servicegroups', 'icingadb/servicegroups'], 'monitoring/list/contactgroups' => ['contactgroups', 'icingadb/contactgroups'], 'monitoring/list/contacts' => ['contacts', 'icingadb/contacts'], 'monitoring/list/comments' => ['comments', 'icingadb/comments'], 'monitoring/list/downtimes' => ['downtimes', 'icingadb/downtimes'], 'monitoring/list/eventhistory' => ['history', 'icingadb/history'], 'monitoring/list/notifications' => ['notificationHistory', 'icingadb/notifications'], 'monitoring/health/info' => [null, 'icingadb/health'], 'monitoring/health/stats' => [null, 'icingadb/health'], 'monitoring/tactical' => ['services', 'icingadb/tactical'] ]; public static function isSupportedUrl(Url $url): bool { $supportedPaths = self::SUPPORTED_PATHS; return isset($supportedPaths[ltrim($url->getPath(), '/')]); } public static function hasParamTransformer(string $name): bool { return method_exists(new self(), $name . 'Parameters'); } public static function hasQueryTransformer(string $name): bool { return method_exists(new self(), $name . 'Columns'); } public static function transformUrl(Url $url): Url { if (! self::isSupportedUrl($url)) { throw new InvalidArgumentException(sprintf('Url path "%s" is not supported', $url->getPath())); } list($transformer, $dbRoute) = self::SUPPORTED_PATHS[ltrim($url->getPath(), '/')]; $url = clone $url; $url->setPath($dbRoute); if (! $url->getParams()->isEmpty()) { [$params, $filter] = self::transformParams($url, $transformer); $url->setParams($params); if (! $filter->isEmpty()) { $filter = QueryString::parse((string) $filter); $filter = self::transformFilter($filter, $transformer); $url->setFilter($filter ?: null); } } return $url; } public static function transformParams(Url $url, ?string $transformerName = null): array { $transformer = new self(); $params = self::commonParameters(); $columns = self::commonColumns(); if ($transformerName !== null) { if (! self::hasQueryTransformer($transformerName)) { throw new InvalidArgumentException(sprintf('Transformer "%s" is not supported', $transformerName)); } if (self::hasParamTransformer($transformerName)) { $params = array_merge($params, $transformer->{$transformerName . 'Parameters'}()); } $columns = array_merge($columns, $transformer->{$transformerName . 'Columns'}()); } $columnRewriter = function ($column) use ($columns, $transformer) { $rewritten = $transformer->rewrite(Filter::equal($column, 'bogus'), $columns); if ($rewritten === false) { return false; } elseif ($rewritten instanceof Filter\Condition) { return $rewritten->getColumn(); } return $column; }; $urlParams = $url->onlyWith(array_keys($params))->getParams(); $urlFilter = $url->without(array_keys($params))->getParams(); $newParams = new UrlParams(); foreach ($urlParams->toArray(false) as $name => $value) { if (is_int($name)) { $name = $value; $value = true; } else { $value = rawurldecode($value); } $name = rawurldecode($name); if (! isset($params[$name]) || $params[$name] === self::USE_EXPR) { $newParams->add($name, $value); } elseif ($params[$name] === self::DROP) { // pass } elseif (is_callable($params[$name])) { $result = $params[$name]($value, $urlParams, $columnRewriter); if ($result === false) { continue; } elseif (is_array($result)) { [$name, $value] = $result; } elseif ($result !== null) { $value = $result; } $newParams->add($name, $value); } } return [$newParams, $urlFilter]; } /** * Transform the given legacy filter * * @param Filter\Rule $filter * @param ?string $queryTransformer * * @return Filter\Rule|false */ public static function transformFilter(Filter\Rule $filter, ?string $queryTransformer = null) { $transformer = new self(); $columns = $transformer::commonColumns(); if ($queryTransformer !== null) { if (! self::hasQueryTransformer($queryTransformer)) { throw new InvalidArgumentException(sprintf('Transformer "%s" is not supported', $queryTransformer)); } $columns = array_merge($columns, $transformer->{$queryTransformer . 'Columns'}()); } $rewritten = $transformer->rewrite($filter, $columns); return $rewritten === false ? false : ($rewritten instanceof Filter\Rule ? $rewritten : $filter); } /** * Transform given legacy wildcard filters * * @param $filter Filter\Rule * * @return Filter\Chain|Filter\Condition */ public static function transformLegacyWildcardFilter(Filter\Rule $filter) { if ($filter instanceof Filter\Chain) { foreach ($filter as $child) { $newChild = self::transformLegacyWildcardFilter($child); if ($newChild !== $child) { $filter->replace($child, $newChild); } } return $filter; } else { /** @var Filter\Condition $filter */ return self::transformWildcardFilter($filter); } } /** * Rewrite the given filter and legacy columns * * @param Filter\Rule $filter * @param array $legacyColumns * * @return ?mixed */ protected function rewrite(Filter\Rule $filter, array $legacyColumns) { $rewritten = null; if ($filter instanceof Filter\Condition) { $column = $filter->getColumn(); $modelPath = null; $exprRule = null; if (isset($legacyColumns[$column])) { if ($legacyColumns[$column] === self::DROP) { return false; } elseif (is_callable($legacyColumns[$column])) { return $legacyColumns[$column]($filter); } elseif (! is_array($legacyColumns[$column])) { return null; } foreach ($legacyColumns[$column] as $modelPath => $exprRule) { break; } $rewritten = $filter->setColumn($modelPath); switch (true) { case $exprRule === self::USE_EXPR: break; case $exprRule === self::LOWER_EXPR: $filter->setValue(strtolower($filter->getValue())); break; case is_array($exprRule): if (isset($exprRule[$filter->getValue()])) { $filter->setValue($exprRule[$filter->getValue()]); break; } return false; default: $filter->setValue($exprRule); } $rewritten = self::transformWildcardFilter($rewritten); } elseif (preg_match('/^_(host|service)_(.+)/i', $column, $groups)) { $rewritten = $filter->setColumn($groups[1] . '.vars.' . $groups[2]); $rewritten = self::transformWildcardFilter($rewritten); } } else { /** @var Filter\Chain $filter */ foreach ($filter as $child) { $retVal = $this->rewrite( $child instanceof Filter\Condition ? clone $child : $child, $legacyColumns ); if ($retVal === false) { $filter->remove($child); } elseif ($retVal instanceof Filter\Rule) { $filter->replace($child, $retVal); } } } return $rewritten; } private static function transformWildcardFilter(Filter\Condition $filter) { if (is_string($filter->getValue()) && strpos($filter->getValue(), '*') !== false) { if ($filter instanceof Filter\Equal) { return Filter::like($filter->getColumn(), $filter->getValue()); } elseif ($filter instanceof Filter\Unequal) { return Filter::unlike($filter->getColumn(), $filter->getValue()); } } return $filter; } protected static function commonParameters(): array { return [ 'sort' => function ($value, $params, $rewriter) { $value = $rewriter($value); if ($params->has('dir')) { return "{$value} {$params->get('dir')}"; } return $value; }, 'dir' => self::DROP, 'limit' => self::USE_EXPR, 'addFilter' => self::DROP, 'modifyFilter' => self::DROP, 'showCompact' => self::USE_EXPR, 'showFullscreen' => self::USE_EXPR, 'view' => function ($value) { if ($value === 'compact') { return ['showCompact', true]; } return $value; } ]; } protected static function commonColumns(): array { return [ // Filter columns 'host' => [ 'host.name_ci' => self::USE_EXPR ], 'host_display_name' => [ 'host.display_name' => self::USE_EXPR ], 'host_alias' => self::DROP, 'hostgroup' => [ 'hostgroup.name_ci' => self::USE_EXPR ], 'hostgroup_alias' => [ 'hostgroup.display_name' => self::USE_EXPR ], 'service' => [ 'service.name_ci' => self::USE_EXPR ], 'service_display_name' => [ 'service.display_name' => self::USE_EXPR ], 'servicegroup' => [ 'servicegroup.name_ci' => self::USE_EXPR ], 'servicegroup_alias' => [ 'servicegroup.display_name' => self::USE_EXPR ], // Restriction columns 'instance_name' => self::DROP, 'host_name' => [ 'host.name' => self::USE_EXPR ], 'hostgroup_name' => [ 'hostgroup.name' => self::USE_EXPR ], 'service_description' => [ 'service.name' => self::USE_EXPR ], 'servicegroup_name' => [ 'servicegroup.name' => self::USE_EXPR ] ]; } protected static function hostsParameters(): array { return [ 'addColumns' => function ($value, $params, $rewriter) { $legacyColumns = array_filter(array_map('trim', explode(',', $value))); $columns = [ 'host.state.soft_state', 'host.state.last_state_change', 'host.icon_image.icon_image', 'host.display_name', 'host.state.output', 'host.state.performance_data', 'host.state.is_problem' ]; foreach ($legacyColumns as $column) { $column = $rewriter($column); if ($column !== false) { $columns[] = $column; } } return ['columns', implode(',', $columns)]; } ]; } protected static function hostsColumns(): array { return [ // Query columns 'host_acknowledged' => [ 'host.state.is_acknowledged' => self::NO_YES ], 'host_acknowledgement_type' => function ($filter) { $value = $filter->getValue(); if ($value == '2') { return Filter::equal('host.state.is_sticky_acknowledgement', 'y'); } return isset(self::NO_YES[$value]) ? Filter::equal('host.state.is_acknowledged', self::NO_YES[$value]) : false; }, 'host_action_url' => [ 'host.action_url.action_url' => self::USE_EXPR ], 'host_active_checks_enabled' => [ 'host.active_checks_enabled' => self::NO_YES ], 'host_active_checks_enabled_changed' => self::DROP, 'host_address' => [ 'host.address' => self::USE_EXPR ], 'host_address6' => [ 'host.address6' => self::USE_EXPR ], 'host_alias' => self::DROP, 'host_check_command' => [ 'host.checkcommand_name' => self::USE_EXPR ], 'host_check_execution_time' => [ 'host.state.execution_time' => self::USE_EXPR ], 'host_check_latency' => [ 'host.state.latency' => self::USE_EXPR ], 'host_check_source' => [ 'host.state.check_source' => self::USE_EXPR ], 'host_check_timeperiod' => [ 'host.check_timeperiod_name' => self::USE_EXPR ], 'host_current_check_attempt' => [ 'host.state.check_attempt' => self::USE_EXPR ], 'host_current_notification_number' => self::DROP, 'host_display_name' => [ 'host.display_name' => self::USE_EXPR ], 'host_event_handler_enabled' => [ 'host.event_handler_enabled' => self::NO_YES ], 'host_event_handler_enabled_changed' => self::DROP, 'host_flap_detection_enabled' => [ 'host.flapping_enabled' => self::NO_YES ], 'host_flap_detection_enabled_changed' => self::DROP, 'host_handled' => [ 'host.state.is_handled' => self::NO_YES ], 'host_hard_state' => [ 'host.state.hard_state' => self::USE_EXPR ], 'host_in_downtime' => [ 'host.state.in_downtime' => self::NO_YES ], 'host_ipv4' => [ 'host.address_bin' => self::USE_EXPR ], 'host_is_flapping' => [ 'host.state.is_flapping' => self::NO_YES ], 'host_is_reachable' => [ 'host.state.is_reachable' => self::NO_YES ], 'host_last_check' => [ 'host.state.last_update' => self::USE_EXPR ], 'host_last_notification' => self::DROP, 'host_last_state_change' => [ 'host.state.last_state_change' => self::USE_EXPR ], 'host_last_state_change_ts' => [ 'host.state.last_state_change' => self::USE_EXPR ], 'host_long_output' => [ 'host.state.long_output' => self::USE_EXPR ], 'host_max_check_attempts' => [ 'host.max_check_attempts' => self::USE_EXPR ], 'host_modified_host_attributes' => self::DROP, 'host_name' => [ 'host.name' => self::USE_EXPR ], 'host_next_check' => [ 'host.state.next_check' => self::USE_EXPR ], 'host_notes_url' => [ 'host.notes_url.notes_url' => self::USE_EXPR ], 'host_notifications_enabled' => [ 'host.notifications_enabled' => self::NO_YES ], 'host_notifications_enabled_changed' => self::DROP, 'host_obsessing' => self::DROP, 'host_obsessing_changed' => self::DROP, 'host_output' => [ 'host.state.output' => self::USE_EXPR ], 'host_passive_checks_enabled' => [ 'host.passive_checks_enabled' => self::NO_YES ], 'host_passive_checks_enabled_changed' => self::DROP, 'host_percent_state_change' => self::DROP, 'host_perfdata' => [ 'host.state.performance_data' => self::USE_EXPR ], 'host_problem' => [ 'host.state.is_problem' => self::NO_YES ], 'host_severity' => [ 'host.state.severity' => self::USE_EXPR ], 'host_state' => [ 'host.state.soft_state' => self::USE_EXPR ], 'host_state_type' => [ 'host.state.state_type' => ['soft', 'hard'] ], 'host_unhandled' => [ 'host.state.is_handled' => array_reverse(self::NO_YES) ], // Filter columns 'host_contact' => [ 'host.user.name' => self::USE_EXPR ], 'host_contactgroup' => [ 'host.usergroup.name' => self::USE_EXPR ], // Query columns the dataview doesn't include, added here because it's possible to filter for them anyway 'host_check_interval' => self::DROP, 'host_icon_image' => self::DROP, 'host_icon_image_alt' => self::DROP, 'host_notes' => self::DROP, 'object_type' => self::DROP, 'object_id' => self::DROP, 'host_attempt' => self::DROP, 'host_check_type' => self::DROP, 'host_event_handler' => self::DROP, 'host_failure_prediction_enabled' => self::DROP, 'host_is_passive_checked' => self::DROP, 'host_last_hard_state' => self::DROP, 'host_last_hard_state_change' => self::DROP, 'host_last_time_down' => self::DROP, 'host_last_time_unreachable' => self::DROP, 'host_last_time_up' => self::DROP, 'host_next_notification' => self::DROP, 'host_next_update' => function ($filter) { /** @var Filter\Condition $filter */ if ($filter->getValue() !== 'now') { return false; } // Doesn't get dropped because there's a default dashlet using it.. // Though since this dashlet uses it to check for overdue hosts we'll // replace it as next_update is volatile (only in redis up2date) return Filter::equal('host.state.is_overdue', $filter instanceof Filter\LessThan ? 'y' : 'n'); }, 'host_no_more_notifications' => self::DROP, 'host_normal_check_interval' => self::DROP, 'host_problem_has_been_acknowledged' => self::DROP, 'host_process_performance_data' => self::DROP, 'host_retry_check_interval' => self::DROP, 'host_scheduled_downtime_depth' => self::DROP, 'host_status_update_time' => self::DROP, 'problems' => self::DROP ]; } protected static function multipleHostsColumns(): array { return array_merge( static::hostsColumns(), [ 'host' => [ 'host.name' => self::USE_EXPR ] ] ); } protected static function hostColumns(): array { return [ 'host' => [ 'name' => self::USE_EXPR ] ]; } protected static function servicesParameters(): array { return [ 'addColumns' => function ($value, $params, $rewriter) { $legacyColumns = array_filter(array_map('trim', explode(',', $value))); $columns = [ 'service.state.soft_state', 'service.state.last_state_change', 'service.icon_image.icon_image', 'service.display_name', 'service.host.display_name', 'service.state.output', 'service.state.performance_data', 'service.state.is_problem' ]; foreach ($legacyColumns as $column) { $column = $rewriter($column); if ($column !== false) { $columns[] = $column; } } return ['columns', implode(',', $columns)]; } ]; } protected static function servicesColumns(): array { return [ // Query columns 'host_acknowledged' => [ 'host.state.is_acknowledged' => self::NO_YES ], 'host_action_url' => [ 'host.action_url.action_url' => self::USE_EXPR ], 'host_active_checks_enabled' => [ 'host.active_checks_enabled' => self::NO_YES ], 'host_address' => [ 'host.address' => self::USE_EXPR ], 'host_address6' => [ 'host.address6' => self::USE_EXPR ], 'host_alias' => self::DROP, 'host_check_source' => [ 'host.state.check_source' => self::USE_EXPR ], 'host_display_name' => [ 'host.display_name' => self::USE_EXPR ], 'host_handled' => [ 'host.state.is_handled' => self::NO_YES ], 'host_hard_state' => [ 'host.state.hard_state' => self::USE_EXPR ], 'host_in_downtime' => [ 'host.state.in_downtime' => self::NO_YES ], 'host_ipv4' => [ 'host.address_bin' => self::USE_EXPR ], 'host_is_flapping' => [ 'host.state.is_flapping' => self::NO_YES ], 'host_last_check' => [ 'host.state.last_update' => self::USE_EXPR ], 'host_last_hard_state' => [ 'host.state.previous_hard_state' => self::USE_EXPR ], 'host_last_hard_state_change' => self::DROP, 'host_last_state_change' => [ 'host.state.last_state_change' => self::USE_EXPR ], 'host_last_time_down' => self::DROP, 'host_last_time_unreachable' => self::DROP, 'host_last_time_up' => self::DROP, 'host_long_output' => [ 'host.state.long_output' => self::USE_EXPR ], 'host_modified_host_attributes' => self::DROP, 'host_notes_url' => [ 'host.notes_url.notes_url' => self::USE_EXPR ], 'host_notifications_enabled' => [ 'host.notifications_enabled' => self::NO_YES ], 'host_output' => [ 'host.state.output' => self::USE_EXPR ], 'host_passive_checks_enabled' => [ 'host.passive_checks_enabled' => self::NO_YES ], 'host_perfdata' => [ 'host.state.performance_data' => self::USE_EXPR ], 'host_problem' => [ 'host.state.is_problem' => self::NO_YES ], 'host_severity' => [ 'host.state.severity' => self::USE_EXPR ], 'host_state' => [ 'host.state.soft_state' => self::USE_EXPR ], 'host_state_type' => [ 'host.state.state_type' => ['soft', 'hard'] ], 'service_acknowledged' => [ 'service.state.is_acknowledged' => self::NO_YES ], 'service_acknowledgement_type' => function ($filter) { $value = $filter->getValue(); if ($value == '2') { return Filter::equal('service.state.is_sticky_acknowledgement', 'y'); } return isset(self::NO_YES[$value]) ? Filter::equal('service.state.is_acknowledged', self::NO_YES[$value]) : false; }, 'service_action_url' => [ 'service.action_url.action_url' => self::USE_EXPR ], 'service_active_checks_enabled' => [ 'service.active_checks_enabled' => self::NO_YES ], 'service_active_checks_enabled_changed' => self::DROP, 'service_attempt' => [ 'service.state.check_attempt' => self::USE_EXPR ], 'service_check_command' => [ 'service.checkcommand_name' => self::USE_EXPR ], 'service_check_source' => [ 'service.state.check_source' => self::USE_EXPR ], 'service_check_timeperiod' => [ 'service.check_timeperiod_name' => self::USE_EXPR ], 'service_current_check_attempt' => [ 'service.state.check_attempt' => self::USE_EXPR ], 'service_current_notification_number' => self::DROP, 'service_display_name' => [ 'service.display_name' => self::USE_EXPR ], 'service_event_handler_enabled' => [ 'service.event_handler_enabled' => self::NO_YES ], 'service_event_handler_enabled_changed' => self::DROP, 'service_flap_detection_enabled' => [ 'service.flapping_enabled' => self::NO_YES ], 'service_flap_detection_enabled_changed' => self::DROP, 'service_handled' => [ 'service.state.is_handled' => self::NO_YES ], 'service_hard_state' => [ 'service.state.hard_state' => self::USE_EXPR ], 'service_host_name' => [ 'host.name' => self::USE_EXPR ], 'service_in_downtime' => [ 'service.state.in_downtime' => self::NO_YES ], 'service_is_flapping' => [ 'service.state.is_flapping' => self::NO_YES ], 'service_is_reachable' => [ 'service.state.is_reachable' => self::NO_YES ], 'service_last_check' => [ 'service.state.last_update' => self::USE_EXPR ], 'service_last_hard_state' => [ 'service.state.previous_hard_state' => self::USE_EXPR ], 'service_last_hard_state_change' => self::DROP, 'service_last_notification' => self::DROP, 'service_last_state_change' => [ 'service.state.last_state_change' => self::USE_EXPR ], 'service_last_state_change_ts' => [ 'service.state.last_state_change' => self::USE_EXPR ], 'service_last_time_critical' => self::DROP, 'service_last_time_ok' => self::DROP, 'service_last_time_unknown' => self::DROP, 'service_last_time_warning' => self::DROP, 'service_long_output' => [ 'service.state.long_output' => self::USE_EXPR ], 'service_max_check_attempts' => [ 'service.max_check_attempts' => self::USE_EXPR ], 'service_modified_service_attributes' => self::DROP, 'service_next_check' => [ 'service.state.next_check' => self::USE_EXPR ], 'service_notes' => [ 'service.notes' => self::USE_EXPR ], 'service_notes_url' => [ 'service.notes_url.notes_url' => self::USE_EXPR ], 'service_notifications_enabled' => [ 'service.notifications_enabled' => self::NO_YES ], 'service_notifications_enabled_changed' => self::DROP, 'service_obsessing' => self::DROP, 'service_obsessing_changed' => self::DROP, 'service_output' => [ 'service.state.output' => self::USE_EXPR ], 'service_passive_checks_enabled' => [ 'service.passive_checks_enabled' => self::USE_EXPR ], 'service_passive_checks_enabled_changed' => self::DROP, 'service_perfdata' => [ 'service.state.performance_data' => self::USE_EXPR ], 'service_problem' => [ 'service.state.is_problem' => self::NO_YES ], 'service_severity' => [ 'service.state.severity' => self::USE_EXPR ], 'service_state' => [ 'service.state.soft_state' => self::USE_EXPR ], 'service_state_type' => [ 'service.state.state_type' => ['soft', 'hard'] ], 'service_unhandled' => [ 'service.state.is_handled' => array_reverse(self::NO_YES) ], // Filter columns 'host_contact' => [ 'host.user.name' => self::USE_EXPR ], 'host_contactgroup' => [ 'host.usergroup.name' => self::USE_EXPR ], 'service_contact' => [ 'service.user.name' => self::USE_EXPR ], 'service_contactgroup' => [ 'service.usergroup.name' => self::USE_EXPR ], 'service_host' => [ 'host.name_ci' => self::USE_EXPR ], // Query columns the dataview doesn't include, added here because it's possible to filter for them anyway 'host_icon_image' => self::DROP, 'host_icon_image_alt' => self::DROP, 'host_notes' => self::DROP, 'host_acknowledgement_type' => self::DROP, 'host_active_checks_enabled_changed' => self::DROP, 'host_attempt' => self::DROP, 'host_check_command' => self::DROP, 'host_check_execution_time' => self::DROP, 'host_check_latency' => self::DROP, 'host_check_timeperiod_object_id' => self::DROP, 'host_check_type' => self::DROP, 'host_current_check_attempt' => self::DROP, 'host_current_notification_number' => self::DROP, 'host_event_handler' => self::DROP, 'host_event_handler_enabled' => self::DROP, 'host_event_handler_enabled_changed' => self::DROP, 'host_failure_prediction_enabled' => self::DROP, 'host_flap_detection_enabled' => self::DROP, 'host_flap_detection_enabled_changed' => self::DROP, 'host_is_reachable' => self::DROP, 'host_last_notification' => self::DROP, 'host_max_check_attempts' => self::DROP, 'host_next_check' => self::DROP, 'host_next_notification' => self::DROP, 'host_no_more_notifications' => self::DROP, 'host_normal_check_interval' => self::DROP, 'host_notifications_enabled_changed' => self::DROP, 'host_obsessing' => self::DROP, 'host_obsessing_changed' => self::DROP, 'host_passive_checks_enabled_changed' => self::DROP, 'host_percent_state_change' => self::DROP, 'host_problem_has_been_acknowledged' => self::DROP, 'host_process_performance_data' => self::DROP, 'host_retry_check_interval' => self::DROP, 'host_scheduled_downtime_depth' => self::DROP, 'host_status_update_time' => self::DROP, 'host_unhandled' => self::DROP, 'object_type' => self::DROP, 'service_check_interval' => self::DROP, 'service_icon_image' => self::DROP, 'service_icon_image_alt' => self::DROP, 'service_check_execution_time' => self::DROP, 'service_check_latency' => self::DROP, 'service_check_timeperiod_object_id' => self::DROP, 'service_check_type' => self::DROP, 'service_event_handler' => self::DROP, 'service_failure_prediction_enabled' => self::DROP, 'service_is_passive_checked' => self::DROP, 'service_next_notification' => self::DROP, 'service_next_update' => function ($filter) { /** @var Filter\Condition $filter */ if ($filter->getValue() !== 'now') { return false; } // Doesn't get dropped because there's a default dashlet using it.. // Though since this dashlet uses it to check for overdue services we'll // replace it as next_update is volatile (only in redis up2date) return Filter::equal('service.state.is_overdue', $filter instanceof Filter\LessThan ? 'y' : 'n'); }, 'service_no_more_notifications' => self::DROP, 'service_normal_check_interval' => self::DROP, 'service_percent_state_change' => self::DROP, 'service_problem_has_been_acknowledged' => self::DROP, 'service_process_performance_data' => self::DROP, 'service_retry_check_interval' => self::DROP, 'service_scheduled_downtime_depth' => self::DROP, 'service_status_update_time' => self::DROP, 'problems' => self::DROP, ]; } protected static function servicegridColumns(): array { return array_merge( static::servicesColumns(), [ 'problems' => [ 'problems' => self::USE_EXPR ] ] ); } protected static function multipleServicesColumns(): array { return array_merge( static::servicesColumns(), [ 'host' => [ 'host.name' => self::USE_EXPR ], 'service' => [ 'service.name' => self::USE_EXPR ] ] ); } protected static function serviceColumns(): array { return [ 'host' => [ 'host.name' => self::USE_EXPR ], 'service' => [ 'name' => self::USE_EXPR ] ]; } protected static function hostgroupsColumns(): array { return [ // Query columns 'hostgroup_alias' => [ 'hostgroup.display_name' => self::USE_EXPR ], 'hosts_severity' => self::SORT_ONLY, 'hosts_total' => self::SORT_ONLY, 'services_total' => self::SORT_ONLY, // Filter columns 'host_contact' => [ 'host.user.name' => self::USE_EXPR ], 'host_contactgroup' => [ 'host.usergroup.name' => self::USE_EXPR ] ]; } protected static function servicegroupsColumns(): array { return [ // Query columns 'services_severity' => self::SORT_ONLY, 'services_total' => self::SORT_ONLY, 'servicegroup_alias' => [ 'servicegroup.display_name' => self::USE_EXPR ], // Filter columns 'host_contact' => [ 'host.user.name' => self::USE_EXPR ], 'host_contactgroup' => [ 'host.usergroup.name' => self::USE_EXPR ], 'service_contact' => [ 'service.user.name' => self::USE_EXPR ], 'service_contactgroup' => [ 'service.usergroup.name' => self::USE_EXPR ] ]; } protected static function contactgroupsColumns(): array { return [ // Query columns 'contactgroup_name' => [ 'usergroup.name' => self::USE_EXPR ], 'contactgroup_alias' => [ 'usergroup.display_name' => self::USE_EXPR ], 'contact_count' => self::DROP, // Filter columns 'contactgroup' => [ 'usergroup.name_ci' => self::USE_EXPR ] ]; } protected static function contactsColumns(): array { $receivesStateNotifications = function ($state, $type = null) { return function ($filter) use ($state, $type) { /** @var Filter\Condition $filter */ $negate = $filter instanceof Filter\Unequal || $filter instanceof Filter\Unlike; switch ($filter->getValue()) { case '0': $filter = Filter::any( Filter::equal('user.notifications_enabled', 'n'), Filter::unequal('user.states', $state) ); if ($type !== null) { $filter->add(Filter::unequal('user.types', $type)); } break; case '1': $filter = Filter::all( Filter::equal('user.notifications_enabled', 'y'), Filter::equal('user.states', $state) ); if ($type !== null) { $filter->add(Filter::equal('user.types', $type)); } break; default: return null; } if ($negate) { $filter = Filter::none($filter); } return $filter; }; }; return [ // Query columns 'contact_object_id' => self::DROP, 'contact_id' => [ 'user.id' => self::USE_EXPR ], 'contact_name' => [ 'user.name' => self::USE_EXPR ], 'contact_alias' => [ 'user.display_name' => self::USE_EXPR ], 'contact_email' => [ 'user.email' => self::USE_EXPR ], 'contact_pager' => [ 'user.pager' => self::USE_EXPR ], 'contact_has_host_notfications' => $receivesStateNotifications(['up', 'down']), 'contact_has_service_notfications' => $receivesStateNotifications(['ok', 'warning', 'critical', 'unknown']), 'contact_can_submit_commands' => self::DROP, 'contact_notify_service_recovery' => $receivesStateNotifications( ['ok', 'warning', 'critical', 'unknown'], 'recovery' ), 'contact_notify_service_warning' => $receivesStateNotifications('warning'), 'contact_notify_service_critical' => $receivesStateNotifications('critical'), 'contact_notify_service_unknown' => $receivesStateNotifications('unknown'), 'contact_notify_service_flapping' => $receivesStateNotifications( ['ok', 'warning', 'critical', 'unknown'], ['flapping_start', 'flapping_end'] ), 'contact_notify_service_downtime' => $receivesStateNotifications( ['ok', 'warning', 'critical', 'unknown'], ['downtime_start', 'downtime_end', 'downtime_removed'] ), 'contact_notify_host_recovery' => $receivesStateNotifications(['up', 'down'], 'recovery'), 'contact_notify_host_down' => $receivesStateNotifications('down'), 'contact_notify_host_unreachable' => self::DROP, 'contact_notify_host_flapping' => $receivesStateNotifications( ['up', 'down'], ['flapping_start', 'flapping_end'] ), 'contact_notify_host_downtime' => $receivesStateNotifications( ['up', 'down'], ['downtime_start', 'downtime_end', 'downtime_removed'] ), 'contact_notify_host_timeperiod' => function ($filter) { /** @var Filter\Condition $filter */ $filter->setColumn('user.timeperiod.name_ci'); return Filter::all( $filter, Filter::equal('user.states', ['up', 'down']) ); }, 'contact_notify_service_timeperiod' => function ($filter) { /** @var Filter\Condition $filter */ $filter->setColumn('user.timeperiod.name_ci'); return Filter::all( $filter, Filter::equal('user.states', ['ok', 'warning', 'critical', 'unknown']) ); }, // Filter columns 'contact' => [ 'user.name_ci' => self::USE_EXPR ], 'contactgroup' => [ 'usergroup.name_ci' => self::USE_EXPR ], 'contactgroup_name' => [ 'usergroup.name' => self::USE_EXPR ], 'contactgroup_alias' => [ 'usergroup.display_name' => self::USE_EXPR ] ]; } protected static function commentsColumns(): array { return [ // Query columns 'comment_author_name' => [ 'comment.author' => self::USE_EXPR ], 'comment_data' => [ 'comment.text' => self::USE_EXPR ], 'comment_expiration' => [ 'comment.expire_time' => self::USE_EXPR ], 'comment_internal_id' => self::DROP, 'comment_is_persistent' => [ 'comment.is_persistent' => self::NO_YES ], 'comment_name' => [ 'comment.name' => self::USE_EXPR ], 'comment_timestamp' => [ 'comment.entry_time' => self::USE_EXPR ], 'comment_type' => [ 'comment.entry_type' => self::LOWER_EXPR ], 'host_display_name' => [ 'host.display_name' => self::USE_EXPR ], 'object_type' => [ 'comment.object_type' => self::LOWER_EXPR ], 'service_display_name' => [ 'service.display_name' => self::USE_EXPR ], 'service_host_name' => [ 'host.name' => self::USE_EXPR ], // Filter columns 'comment_author' => [ 'comment.author' => self::USE_EXPR ] ]; } protected static function downtimesColumns(): array { return [ // Query columns 'downtime_author_name' => [ 'downtime.author' => self::USE_EXPR ], 'downtime_comment' => [ 'downtime.comment' => self::USE_EXPR ], 'downtime_duration' => [ 'downtime.flexible_duration' => self::USE_EXPR ], 'downtime_end' => [ 'downtime.end_time' => self::USE_EXPR ], 'downtime_entry_time' => [ 'downtime.entry_time' => self::USE_EXPR ], 'downtime_internal_id' => self::DROP, 'downtime_is_fixed' => [ 'downtime.is_flexible' => array_reverse(self::NO_YES) ], 'downtime_is_flexible' => [ 'downtime.is_flexible' => self::NO_YES ], 'downtime_is_in_effect' => [ 'downtime.is_in_effect' => self::NO_YES ], 'downtime_name' => [ 'downtime.name' => self::USE_EXPR ], 'downtime_scheduled_end' => [ 'downtime.scheduled_end_time' => self::USE_EXPR ], 'downtime_scheduled_start' => [ 'downtime.scheduled_start_time' => self::USE_EXPR ], 'downtime_start' => [ 'downtime.start_time' => self::USE_EXPR ], 'host_display_name' => [ 'host.display_name' => self::USE_EXPR ], 'host_state' => [ 'host.state.soft_state' => self::USE_EXPR ], 'object_type' => [ 'downtime.object_type' => self::LOWER_EXPR ], 'service_display_name' => [ 'service.display_name' => self::USE_EXPR ], 'service_host_name' => [ 'host.name' => self::USE_EXPR ], 'service_state' => [ 'service.state.soft_state' => self::USE_EXPR ], // Filter columns 'downtime_author' => [ 'downtime.author' => self::USE_EXPR ] ]; } protected static function historyColumns(): array { return [ // Query columns 'id' => self::DROP, 'object_type' => [ 'history.object_type' => self::LOWER_EXPR ], 'timestamp' => [ 'history.event_time' => self::USE_EXPR ], 'state' => [ 'history.state.soft_state' => self::USE_EXPR ], 'output' => [ 'history.state.output' => self::USE_EXPR ], 'type' => function ($filter) { /** @var Filter\Condition $filter */ $expr = strtolower($filter->getValue()); switch (true) { // NotificationhistoryQuery case substr($expr, 0, 13) === 'notification_': $filter->setColumn('history.notification.type'); $filter->setValue([ 'notification_ack' => 'acknowledgement', 'notification_flapping' => 'flapping_start', 'notification_flapping_end' => 'flapping_end', 'notification_dt_start' => 'downtime_start', 'notification_dt_end' => 'downtime_end', 'notification_custom' => 'custom', 'notification_state' => ['problem', 'recovery'] ][$expr]); return Filter::all($filter, Filter::equal('history.event_type', 'notification')); // StatehistoryQuery case in_array($expr, ['soft_state', 'hard_state'], true): $filter->setColumn('history.state.state_type'); $filter->setValue(substr($expr, 0, 4)); return Filter::all($filter, Filter::equal('history.event_type', 'state_change')); // DowntimestarthistoryQuery and DowntimeendhistoryQuery case in_array($expr, ['dt_start', 'dt_end'], true): $filter->setColumn('history.event_type'); $filter->setValue('downtime_' . substr($expr, 3)); return $filter; // CommenthistoryQuery case in_array($expr, ['comment', 'ack'], true): $filter->setColumn('history.comment.entry_type'); $filter->setValue($expr); return Filter::all($filter, Filter::equal('history.event_type', 'comment_add')); // CommentdeletionhistoryQuery case in_array($expr, ['comment_deleted', 'ack_deleted'], true): $filter->setColumn('history.comment.entry_type'); $filter->setValue($expr); return Filter::all($filter, Filter::equal('history.event_type', 'comment_remove')); // FlappingstarthistoryQuery and CommenthistoryQuery case in_array($expr, ['flapping', 'flapping_deleted'], true): $filter->setColumn('history.event_type'); return $filter->setValue($expr === 'flapping' ? 'flapping_start' : 'flapping_end'); } } ]; } protected static function notificationHistoryColumns(): array { return [ // Query columns 'notification_contact_name' => [ 'notification_history.user.name' => self::USE_EXPR ], 'notification_output' => [ 'notification_history.text' => self::USE_EXPR ], 'notification_reason' => [ 'notification_history.type' => [ 0 => ['problem', 'recovery'], 1 => 'acknowledgement', 2 => 'flapping_start', 3 => 'flapping_end', 5 => 'downtime_start', 6 => 'downtime_end', 7 => 'downtime_removed', 8 => 'custom' // ido schema doc says it's `99`, icinga2 though uses `8` ] ], 'notification_state' => [ 'notification_history.state' => self::USE_EXPR ], 'notification_timestamp' => [ 'notification_history.send_time' => self::USE_EXPR ], 'object_type' => [ 'notification_history.object_type' => self::LOWER_EXPR ], 'service_host_name' => [ 'host.name' => self::USE_EXPR ] ]; } } icingadb-web-1.4.0/library/Icingadb/Data/000077500000000000000000000000001516124654200200525ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Data/CsvResultSet.php000066400000000000000000000003721516124654200231730ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Data; use ipl\Orm\ResultSet; class CsvResultSet extends ResultSet { use CsvResultSetUtils; } icingadb-web-1.4.0/library/Icingadb/Data/CsvResultSetUtils.php000066400000000000000000000061751516124654200242230ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Data; use DateTime; use DateTimeZone; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use ipl\Orm\Model; use ipl\Orm\Query; trait CsvResultSetUtils { /** * @return array */ public function current(): array { return $this->extractKeysAndValues(parent::current()); } protected function formatValue(string $key, $value): ?string { if ( $value && ( $key === 'id' || substr($key, -3) === '_id' || substr($key, -3) === '.id' || substr($key, -9) === '_checksum' || substr($key, -4) === '_bin' ) ) { $value = bin2hex($value); } if (is_bool($value)) { return $value ? 'true' : 'false'; } elseif (is_string($value)) { return '"' . str_replace('"', '""', $value) . '"'; } elseif (is_array($value)) { return '"' . implode(',', $value) . '"'; } elseif ($value instanceof DateTime) { return $value->setTimezone(new DateTimeZone('UTC')) ->format('Y-m-d\TH:i:s.vP'); } else { return $value; } } protected function extractKeysAndValues(Model $model, string $path = ''): array { $keysAndValues = []; foreach ($model as $key => $value) { $keyPath = ($path ? $path . '.' : '') . $key; if ($value instanceof Model) { $keysAndValues += $this->extractKeysAndValues($value, $keyPath); } else { $keysAndValues[$keyPath] = $this->formatValue($key, $value); } } return $keysAndValues; } public static function stream(Query $query): void { $model = $query->getModel(); if ($model instanceof Host || $model instanceof Service || $model instanceof DependencyNode) { $query->setResultSetClass(VolatileCsvResults::class); } else { $query->setResultSetClass(__CLASS__); } if ($query->hasLimit()) { // Custom limits should still apply $query->peekAhead(false); $offset = $query->getOffset(); } else { $query->limit(1000); $query->peekAhead(); $offset = 0; } $headerPrinted = false; do { $query->offset($offset); $result = $query->execute()->disableCache(); foreach ($result as $keysAndValues) { if (! $headerPrinted) { echo implode(',', array_keys($keysAndValues)); $headerPrinted = true; } echo "\r\n"; echo implode(',', array_values($keysAndValues)); JsonResultSet::giveMeMoreTime(); } $offset += 1000; } while ($result->hasMore()); exit; } } icingadb-web-1.4.0/library/Icingadb/Data/DependencyNodes.php000066400000000000000000000037431516124654200236410ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Data; use ArrayIterator; use Countable; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Host; use ipl\Stdlib\Filter; use IteratorAggregate; /** * This class is used for {@see MultiselectQuickActions} so it can support objects of type Host * and Service at the same time. * * It fulfils the return type of {@see CommandActions::fetchCommandTargets()} by providing the * method getFilter() and implementing countable and iterable. * * @internal It must only be used in icingadb-web */ class DependencyNodes implements IteratorAggregate, Countable { use Database; use Auth; protected $nodes; /** @var Filter\Rule */ protected $filter; public function __construct(Filter\Rule $filter) { $this->filter = $filter; } public function getIterator(): ArrayIterator { if ($this->nodes === null) { $membersQuery = DependencyNode::on($this->getDb()) ->with([ 'host', 'host.state', 'service', 'service.state', 'service.host' ]) ->filter($this->filter); $this->applyRestrictions($membersQuery); $nodes = []; foreach ($membersQuery as $node) { $nodes[] = $node->service_id !== null ? $node->service : $node->host; } $this->nodes = new ArrayIterator($nodes); } return $this->nodes; } public function getFilter(): Filter\Rule { return $this->filter; } public function count(): int { return $this->getIterator()->count(); } public function getModel() { return new Host(); } } icingadb-web-1.4.0/library/Icingadb/Data/JsonResultSet.php000066400000000000000000000003741516124654200233530ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Data; use ipl\Orm\ResultSet; class JsonResultSet extends ResultSet { use JsonResultSetUtils; } icingadb-web-1.4.0/library/Icingadb/Data/JsonResultSetUtils.php000066400000000000000000000100371516124654200243710ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Data; use DateTime; use DateTimeZone; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Util\Json; use ipl\Orm\Model; use ipl\Orm\Query; trait JsonResultSetUtils { /** * @return array */ public function current(): array { return $this->createObject(parent::current()); } protected function formatValue(string $key, $value) { if ( $value && ( $key === 'id' || substr($key, -3) === '_id' || substr($key, -3) === '.id' || substr($key, -9) === '_checksum' || substr($key, -4) === '_bin' ) ) { $value = bin2hex($value); } if ($value instanceof DateTime) { return $value->setTimezone(new DateTimeZone('UTC')) ->format('Y-m-d\TH:i:s.vP'); } return $value; } protected function createObject(Model $model): array { $keysAndValues = []; foreach ($model as $key => $value) { if ($value instanceof Model) { $object = $this->createObject($value); // If there is no value in the model or it's descendents, // it was not a part of the query, so no JSON object will be created for this model. if (! empty($object)) { $keysAndValues[$key] = $object; } } else { $keysAndValues[$key] = $this->formatValue($key, $value); } } return $keysAndValues; } public static function stream(Query $query): void { $model = $query->getModel(); if ($model instanceof Host || $model instanceof Service || $model instanceof DependencyNode) { $query->setResultSetClass(VolatileJsonResults::class); } else { $query->setResultSetClass(__CLASS__); } if ($query->hasLimit()) { // Custom limits should still apply $query->peekAhead(false); $offset = $query->getOffset() ?? 0; } else { $query->limit(1000); $query->peekAhead(); $offset = 0; } $first = true; echo '['; do { $query->offset($offset); $result = $query->execute()->disableCache(); foreach ($result as $object) { if ($first) { $first = false; } else { echo ",\n"; } echo Json::sanitize($object); self::giveMeMoreTime(); } $offset += 1000; } while ($result->hasMore()); echo ']'; exit; } /** * Grant the caller more time to work with * * This resets the execution time before it runs out. The advantage of this, compared with no execution time * limit at all, is that only the caller can bypass the limit. Any other (faulty) code will still be stopped. * * @internal Don't use outside of {@see JsonResultSet::stream()} or {@see CsvResultSet::stream()} * * @return void */ public static function giveMeMoreTime() { $spent = getrusage(); if ($spent !== false) { $maxExecutionTime = ini_get('max_execution_time'); if (! $maxExecutionTime || ! is_numeric($maxExecutionTime)) { $maxExecutionTime = 30; } else { $maxExecutionTime = (int) $maxExecutionTime; } if ($maxExecutionTime > 0) { $timeRemaining = $maxExecutionTime - $spent['ru_utime.tv_sec'] % $maxExecutionTime; if ($timeRemaining <= 5) { set_time_limit($maxExecutionTime); } } } } } icingadb-web-1.4.0/library/Icingadb/Data/PivotTable.php000066400000000000000000000314351516124654200226420ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Data; use Icinga\Application\Icinga; use Icinga\Application\Web; use ipl\Orm\Query; use ipl\Stdlib\Contract\Paginatable; use ipl\Stdlib\Filter; class PivotTable { public const SORT_ASC = 'asc'; /** * The query to fetch as pivot table * * @var Query */ protected $baseQuery; /** * X-axis pivot column * * @var string */ protected $xAxisColumn; /** * Y-axis pivot column * * @var string */ protected $yAxisColumn; /** * The filter being applied on the query for the x-axis * * @var Filter\Rule */ protected $xAxisFilter; /** * The filter being applied on the query for the y-axis * * @var Filter\Rule */ protected $yAxisFilter; /** * The query to fetch the leading x-axis rows and their headers * * @var Query */ protected $xAxisQuery; /** * The query to fetch the leading y-axis rows and their headers * * @var Query */ protected $yAxisQuery; /** * X-axis header column * * @var string|null */ protected $xAxisHeader; /** * Y-axis header column * * @var string|null */ protected $yAxisHeader; /** * Order by column and direction * * @var array */ protected $order = []; /** * Grid columns as [Alias => Column name] pairs * * @var array */ protected $gridcols = []; /** * Create a new pivot table * * @param Query $query The query to fetch as pivot table * @param string $xAxisColumn X-axis pivot column * @param string $yAxisColumn Y-axis pivot column * @param array $gridcols Grid columns */ public function __construct(Query $query, string $xAxisColumn, string $yAxisColumn, array $gridcols) { foreach ($query->getOrderBy() as $sort) { $this->order[$sort[0]] = $sort[1]; } $this->baseQuery = $query->columns($gridcols)->resetOrderBy(); $this->xAxisColumn = $xAxisColumn; $this->yAxisColumn = $yAxisColumn; $this->gridcols = $gridcols; } /** * Set the filter to apply on the query for the x-axis * * @param ?Filter\Rule $filter * * @return $this */ public function setXAxisFilter(?Filter\Rule $filter = null): self { $this->xAxisFilter = $filter; return $this; } /** * Set the filter to apply on the query for the y-axis * * @param ?Filter\Rule $filter * * @return $this */ public function setYAxisFilter(?Filter\Rule $filter = null): self { $this->yAxisFilter = $filter; return $this; } /** * Get the x-axis header * * Defaults to {@link $xAxisColumn} in case no x-axis header has been set using {@link setXAxisHeader()} * * @return string */ public function getXAxisHeader(): string { if ($this->xAxisHeader === null && $this->xAxisColumn === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->xAxisHeader !== null ? $this->xAxisHeader : $this->xAxisColumn; } /** * Set the x-axis header * * @param string $xAxisHeader * * @return $this */ public function setXAxisHeader(string $xAxisHeader): self { $this->xAxisHeader = $xAxisHeader; return $this; } /** * Get the y-axis header * * Defaults to {@link $yAxisColumn} in case no x-axis header has been set using {@link setYAxisHeader()} * * @return string */ public function getYAxisHeader(): string { if ($this->yAxisHeader === null && $this->yAxisColumn === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->yAxisHeader !== null ? $this->yAxisHeader : $this->yAxisColumn; } /** * Set the y-axis header * * @param string $yAxisHeader * * @return $this */ public function setYAxisHeader(string $yAxisHeader): self { $this->yAxisHeader = $yAxisHeader; return $this; } /** * Return the value for the given request parameter * * @param string $axis The axis for which to return the parameter ('x' or 'y') * @param string $param The parameter name to return * @param ?int $default The default value to return * * @return int */ protected function getPaginationParameter(string $axis, string $param, ?int $default = null): int { /** @var Web $app */ $app = Icinga::app(); $value = $app->getRequest()->getParam($param, ''); if (strpos($value, ',') > 0) { $parts = explode(',', $value, 2); return intval($parts[$axis === 'x' ? 0 : 1]); } return $default !== null ? $default : 0; } /** * Query horizontal (x) axis * * @return Query */ protected function queryXAxis(): Query { if ($this->xAxisQuery === null) { $this->xAxisQuery = clone $this->baseQuery; $xAxisHeader = $this->getXAxisHeader(); $table = $this->xAxisQuery->getModel()->getTableName(); $xCol = explode('.', $this->gridcols[$this->xAxisColumn]); $columns = [ $this->xAxisColumn => $this->gridcols[$this->xAxisColumn], $xAxisHeader => $this->gridcols[$xAxisHeader] ]; // TODO: This shouldn't be required. Refactor this once ipl\Orm\Query has support for group by rules! if ($xCol[0] !== $table) { $groupCols = array_unique([ $this->xAxisColumn => $table . '_' . $this->gridcols[$this->xAxisColumn], $xAxisHeader => $table . '_' . $this->gridcols[$xAxisHeader] ]); } else { $groupCols = $columns; } $this->xAxisQuery->getSelectBase()->groupBy($groupCols); if (count($columns) !== 2) { $columns[] = $this->gridcols[$xAxisHeader]; } $this->xAxisQuery->columns($columns); if ($this->xAxisFilter !== null) { $this->xAxisQuery->filter($this->xAxisFilter); } $this->xAxisQuery->orderBy( $this->gridcols[$xAxisHeader], isset($this->order[$this->gridcols[$xAxisHeader]]) ? $this->order[$this->gridcols[$xAxisHeader]] : self::SORT_ASC ); } return $this->xAxisQuery; } /** * Query vertical (y) axis * * @return Query */ protected function queryYAxis(): Query { if ($this->yAxisQuery === null) { $this->yAxisQuery = clone $this->baseQuery; $yAxisHeader = $this->getYAxisHeader(); $table = $this->yAxisQuery->getModel()->getTableName(); $columns = [ $this->yAxisColumn => $this->gridcols[$this->yAxisColumn], $yAxisHeader => $this->gridcols[$yAxisHeader] ]; $yCol = explode('.', $this->gridcols[$this->yAxisColumn]); // TODO: This shouldn't be required. Refactor this once ipl\Orm\Query has support for group by rules! if ($yCol[0] !== $table) { $groupCols = array_unique([ $this->yAxisColumn => $table . '_' . $this->gridcols[$this->yAxisColumn], $yAxisHeader => $table . '_' . $this->gridcols[$yAxisHeader] ]); } else { $groupCols = $columns; } $this->yAxisQuery->getSelectBase()->groupBy($groupCols); if (count($columns) !== 2) { $columns[] = $this->gridcols[$yAxisHeader]; } $this->yAxisQuery->columns($columns); if ($this->yAxisFilter !== null) { $this->yAxisQuery->filter($this->yAxisFilter); } $this->yAxisQuery->orderBy( $this->gridcols[$yAxisHeader], isset($this->order[$this->gridcols[$yAxisHeader]]) ? $this->order[$this->gridcols[$yAxisHeader]] : self::SORT_ASC ); } return $this->yAxisQuery; } /** * Return a pagination adapter for the x-axis query * * $limit and $page are taken from the current request if not given. * * @param ?int $limit The maximum amount of entries to fetch * @param ?int $page The page to set as current one * * @return Paginatable */ public function paginateXAxis(?int $limit = null, ?int $page = null): Paginatable { if ($limit === null || $page === null) { if ($limit === null) { $limit = $this->getPaginationParameter('x', 'limit', 20); } if ($page === null) { $page = $this->getPaginationParameter('x', 'page', 1); } } $query = $this->queryXAxis(); if ($limit !== 0) { $query->limit($limit); $query->offset($page > 0 ? ($page - 1) * $limit : 0); } return $query; } /** * Return a Paginatable for the y-axis query * * $limit and $page are taken from the current request if not given. * * @param ?int $limit The maximum amount of entries to fetch * @param ?int $page The page to set as current one * * @return Paginatable */ public function paginateYAxis(?int $limit = null, ?int $page = null): Paginatable { if ($limit === null || $page === null) { if ($limit === null) { $limit = $this->getPaginationParameter('y', 'limit', 20); } if ($page === null) { $page = $this->getPaginationParameter('y', 'page', 1); } } $query = $this->queryYAxis(); if ($limit !== 0) { $query->limit($limit); $query->offset($page > 0 ? ($page - 1) * $limit : 0); } return $query; } /** * Return the pivot table as an array of pivot data and pivot header * * @return array */ public function toArray(): array { if ( ($this->xAxisFilter === null && $this->yAxisFilter === null) || ($this->xAxisFilter !== null && $this->yAxisFilter !== null) ) { $xAxis = $this->queryXAxis()->getDb()->fetchPairs($this->queryXAxis()->assembleSelect()); $xAxisKeys = array_keys($xAxis); $yAxis = $this->queryYAxis()->getDb()->fetchPairs($this->queryYAxis()->assembleSelect()); $yAxisKeys = array_keys($yAxis); } else { if ($this->xAxisFilter !== null) { $xAxis = $this->queryXAxis()->getDb()->fetchPairs($this->queryXAxis()->assembleSelect()); $xAxisKeys = array_keys($xAxis); $yQuery = $this->queryYAxis(); $yQuery->filter(Filter::equal($this->gridcols[$this->xAxisColumn], $xAxisKeys)); $yAxis = $this->queryYAxis()->getDb()->fetchPairs($this->queryYAxis()->assembleSelect()); $yAxisKeys = array_keys($yAxis); } else { // $this->yAxisFilter !== null $yAxis = $this->queryYAxis()->getDb()->fetchPairs($this->queryYAxis()->assembleSelect()); $yAxisKeys = array_keys($yAxis); $xQuery = $this->queryXAxis(); $xQuery->filter(Filter::equal($this->gridcols[$this->yAxisColumn], $yAxisKeys)); $xAxis = $this->queryXAxis()->getDb()->fetchPairs($this->queryXAxis()->assembleSelect()); $xAxisKeys = array_keys($yAxis); } } $pivotData = []; $pivotHeader = [ 'cols' => $xAxis, 'rows' => $yAxis ]; if (! empty($xAxis) && ! empty($yAxis)) { $this->baseQuery->filter(Filter::equal($this->gridcols[$this->xAxisColumn], $xAxisKeys)); $this->baseQuery->filter(Filter::equal($this->gridcols[$this->yAxisColumn], $yAxisKeys)); foreach ($yAxisKeys as $yAxisKey) { foreach ($xAxisKeys as $xAxisKey) { $pivotData[$yAxisKey][$xAxisKey] = null; } } foreach ($this->baseQuery as $row) { $pivotData[$row->{$this->yAxisColumn}][$row->{$this->xAxisColumn}] = $row; } } return [$pivotData, $pivotHeader]; } } icingadb-web-1.4.0/library/Icingadb/Data/VolatileCsvResults.php000066400000000000000000000006171516124654200244040ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Data; use Icinga\Module\Icingadb\Redis\VolatileStateResults; /** * @internal This class is supposed to be used by {@see CsvResultSet::stream()} only. */ final class VolatileCsvResults extends VolatileStateResults { use CsvResultSetUtils; } icingadb-web-1.4.0/library/Icingadb/Data/VolatileJsonResults.php000066400000000000000000000006221516124654200245560ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Data; use Icinga\Module\Icingadb\Redis\VolatileStateResults; /** * @internal This class is supposed to be used by {@see JsonResultSet::stream()} only. */ final class VolatileJsonResults extends VolatileStateResults { use JsonResultSetUtils; } icingadb-web-1.4.0/library/Icingadb/Hook/000077500000000000000000000000001516124654200201015ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Hook/ActionsHook/000077500000000000000000000000001516124654200223225ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Hook/ActionsHook/ObjectActionsHook.php000066400000000000000000000057471516124654200264200ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook\ActionsHook; use Icinga\Application\Hook; use Icinga\Application\Logger; use Icinga\Exception\IcingaException; use Icinga\Module\Icingadb\Hook\Common\HookUtils; use Icinga\Module\Icingadb\Hook\HostActionsHook; use Icinga\Module\Icingadb\Hook\ServiceActionsHook; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use InvalidArgumentException; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Html\HtmlString; use ipl\Html\Text; use ipl\Orm\Model; use ipl\Web\Widget\Link; use Throwable; use function ipl\Stdlib\get_php_type; abstract class ObjectActionsHook { use HookUtils; /** * Load all actions for the given object * * @param Host|Service $object * * @return HtmlElement * * @throws InvalidArgumentException If the given model is not supported */ final public static function loadActions(Model $object): HtmlElement { switch (true) { case $object instanceof Host: $hookName = 'Icingadb\\HostActions'; break; case $object instanceof Service: $hookName = 'Icingadb\\ServiceActions'; break; default: throw new InvalidArgumentException( sprintf('%s is not a supported object type', get_php_type($object)) ); } $list = new HtmlElement('ul', Attributes::create(['class' => 'object-detail-actions'])); /** @var HostActionsHook|ServiceActionsHook $hook */ foreach (Hook::all($hookName) as $hook) { try { foreach ($hook->getActionsForObject($object) as $link) { if (! $link instanceof Link) { continue; } if ($link->getBaseTarget() === null && ! $link->hasAttribute('target')) { $link->setBaseTarget('_next'); } // It may be ValidHtml, but modules shouldn't be able to break our views. // That's why it needs to be rendered instantly, as any error will then // be caught here. $renderedLink = (string) $link; $moduleName = $hook->getModule()->getName(); $list->addHtml(new HtmlElement('li', Attributes::create([ 'class' => 'icinga-module module-' . $moduleName, 'data-icinga-module' => $moduleName ]), HtmlString::create($renderedLink))); } } catch (Throwable $e) { Logger::error("Failed to load object actions: %s\n%s", $e, $e->getTraceAsString()); $list->addHtml(new HtmlElement('li', null, Text::create(IcingaException::describe($e)))); } } return $list; } } icingadb-web-1.4.0/library/Icingadb/Hook/Common/000077500000000000000000000000001516124654200213315ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Hook/Common/HookUtils.php000066400000000000000000000015371516124654200237710ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook\Common; use Icinga\Application\ClassLoader; use Icinga\Application\Icinga; use Icinga\Application\Modules\Module; trait HookUtils { final public function __construct() { $this->init(); } /** * Initialize this hook * * Override this in your concrete implementation for any initialization at construction time. */ protected function init() { } /** * Get the module this hook belongs to * * @return Module */ final public function getModule(): Module { $moduleName = ClassLoader::extractModuleName(static::class); return Icinga::app()->getModuleManager() ->getModule($moduleName); } } icingadb-web-1.4.0/library/Icingadb/Hook/Common/TotalSlaReportUtils.php000066400000000000000000000031251516124654200260030ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook\Common; use Icinga\Module\Icingadb\ProvidedHook\Reporting\HostSlaReport; use Icinga\Module\Reporting\Timerange; use ipl\Html\Html; use ipl\Web\Widget\EmptyState; use function ipl\I18n\t; trait TotalSlaReportUtils { public function getHtml(Timerange $timerange, ?array $config = null) { $data = $this->getData($timerange, $config); $count = $data->count(); if (! $count) { return new EmptyState(t('No data found.')); } $threshold = (float) ($config['threshold'] ?? static::DEFAULT_THRESHOLD); $tableRows = []; $precision = $config['sla_precision'] ?? static::DEFAULT_REPORT_PRECISION; // We only have one average $average = $data->getAverages()[0]; if ($average < $threshold) { $slaClass = 'nok'; } else { $slaClass = 'ok'; } $total = $this instanceof HostSlaReport ? sprintf(t('Total (%d Hosts)'), $count) : sprintf(t('Total (%d Services)'), $count); $tableRows[] = Html::tag('tr', null, [ Html::tag('td', ['colspan' => count($data->getDimensions())], $total), Html::tag('td', ['class' => "sla-column $slaClass"], round($average, $precision)) ]); $table = Html::tag( 'table', ['class' => 'common-table sla-table'], [Html::tag('tbody', null, $tableRows)] ); return $table; } } icingadb-web-1.4.0/library/Icingadb/Hook/CustomVarRendererHook.php000066400000000000000000000057661516124654200250630ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Closure; use Icinga\Application\Hook; use Icinga\Application\Logger; use Icinga\Module\Icingadb\Hook\Common\HookUtils; use ipl\Orm\Model; use Throwable; abstract class CustomVarRendererHook { use HookUtils; /** * Prefetch the data the hook needs to render custom variables * * @param Model $object The object for which they'll be rendered * * @return bool Return true if the hook can render variables for the given object, false otherwise */ abstract public function prefetchForObject(Model $object): bool; /** * Render the given variable name * * @param string $key * * @return ?mixed */ abstract public function renderCustomVarKey(string $key); /** * Render the given variable value * * @param string $key * @param mixed $value * * @return ?mixed */ abstract public function renderCustomVarValue(string $key, $value); /** * Return a group name for the given variable name * * @param string $key * * @return ?string */ abstract public function identifyCustomVarGroup(string $key): ?string; /** * Prepare available hooks to render custom variables of the given object * * @param Model $object * * @return Closure A callback ($key, $value) which returns an array [$newKey, $newValue, $group] */ final public static function prepareForObject(Model $object): Closure { $hooks = []; foreach (Hook::all('Icingadb/CustomVarRenderer') as $hook) { /** @var self $hook */ try { if ($hook->prefetchForObject($object)) { $hooks[] = $hook; } } catch (Throwable $e) { Logger::error('Failed to load hook %s: %s', get_class($hook), $e); Logger::debug($e); } } return function (string $key, $value) use ($hooks) { $newKey = $key; $newValue = $value; $group = null; foreach ($hooks as $hook) { /** @var self $hook */ try { $renderedKey = $hook->renderCustomVarKey($key); $renderedValue = $hook->renderCustomVarValue($key, $value); $group = $hook->identifyCustomVarGroup($key); } catch (Throwable $e) { Logger::error('Failed to use hook %s: %s', get_class($hook), $e); Logger::debug($e); continue; } if ($renderedKey !== null || $renderedValue !== null) { $newKey = $renderedKey ?? $key; $newValue = $renderedValue ?? $value; break; } } return [$newKey, $newValue, $group]; }; } } icingadb-web-1.4.0/library/Icingadb/Hook/EventDetailExtensionHook.php000066400000000000000000000011331516124654200255320ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\History; use ipl\Html\ValidHtml; abstract class EventDetailExtensionHook extends ObjectDetailExtensionHook { /** * Assemble and return an HTML representation of the given event * * @param History $event * * @return ValidHtml */ abstract public function getHtmlForObject(History $event): ValidHtml; } icingadb-web-1.4.0/library/Icingadb/Hook/ExtensionHook/000077500000000000000000000000001516124654200226765ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Hook/ExtensionHook/BaseExtensionHook.php000066400000000000000000000102771516124654200270060ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook\ExtensionHook; use Icinga\Module\Icingadb\Hook\Common\HookUtils; abstract class BaseExtensionHook { use HookUtils; /** @var int Used as default return value for {@see BaseExtensionHook::getLocation()} */ public const IDENTIFY_LOCATION_BY_SECTION = -1; /** @var string Output section, right at the top */ public const OUTPUT_SECTION = 'output'; /** @var string Graph section, below output */ public const GRAPH_SECTION = 'graph'; /** @var string Detail section, below graphs */ public const DETAIL_SECTION = 'detail'; /** @var string Action section, below action and note urls */ public const ACTION_SECTION = 'action'; /** @var string Problem section, below comments and downtimes */ public const PROBLEM_SECTION = 'problem'; /** @var string Related section, below groups and notification recipients */ public const RELATED_SECTION = 'related'; /** @var string State section, below check statistics and performance data */ public const STATE_SECTION = 'state'; /** @var string Config section, below custom variables and feature toggles */ public const CONFIG_SECTION = 'config'; /** * Base locations for all known sections * * @var array */ public const BASE_LOCATIONS = [ self::OUTPUT_SECTION => 1000, self::GRAPH_SECTION => 1100, self::DETAIL_SECTION => 1200, self::ACTION_SECTION => 1300, self::PROBLEM_SECTION => 1400, self::RELATED_SECTION => 1500, self::STATE_SECTION => 1600, self::CONFIG_SECTION => 1700 ]; /** @var int This hook's location */ private $location = self::IDENTIFY_LOCATION_BY_SECTION; /** @var string This hook's section */ private $section = self::DETAIL_SECTION; /** * Set this hook's location * * Note that setting the location explicitly may override other widgets using the same location. But beware that * this applies to this hook's widget as well. * * Also, while the sections are guaranteed to always refer to the same general location, this guarantee is lost * when setting a location explicitly. The core and base locations may change at any time and any explicitly set * location will **not** adjust accordingly. * * @param int $location * * @return void */ final public function setLocation(int $location) { $this->location = $location; } /** * Get this hook's location * * @return int */ final public function getLocation(): int { return $this->location; } /** * Set this hook's section * * Sections are used to place widgets loosely in a general location. Using e.g. the `state` section this hook's * widget will always appear after the check statistics and performance data widgets. * * @param string $section * * @return void */ final public function setSection(string $section) { $this->section = $section; } /** * Get this hook's section * * @return string */ final public function getSection(): string { return $this->section; } /** * Union both arrays and sort the result by key * * @param array $coreElements * @param array $extensions * * @return array */ final public static function injectExtensions(array $coreElements, array $extensions): array { $extensions += $coreElements; uksort($extensions, function ($a, $b) { if ($a < 1000 && $b >= 1000) { $b -= 1000; if (abs($a - $b) < 10 && abs($a % 100 - $b % 100) < 10) { return -1; } } elseif ($b < 1000 && $a >= 1000) { $a -= 1000; if (abs($a - $b) < 10 && abs($a % 100 - $b % 100) < 10) { return 1; } } return $a < $b ? -1 : ($a > $b ? 1 : 0); }); return $extensions; } } icingadb-web-1.4.0/library/Icingadb/Hook/ExtensionHook/ObjectDetailExtensionHook.php000066400000000000000000000116161516124654200304630ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook\ExtensionHook; use Icinga\Application\Hook; use Icinga\Application\Logger; use Icinga\Exception\IcingaException; use Icinga\Module\Icingadb\Hook\EventDetailExtensionHook; use Icinga\Module\Icingadb\Hook\HostDetailExtensionHook; use Icinga\Module\Icingadb\Hook\ServiceDetailExtensionHook; use Icinga\Module\Icingadb\Hook\UserDetailExtensionHook; use Icinga\Module\Icingadb\Hook\UsergroupDetailExtensionHook; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\RedundancyGroup; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Model\Usergroup; use InvalidArgumentException; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Html\HtmlString; use ipl\Html\Text; use ipl\Html\ValidHtml; use ipl\Orm\Model; use Throwable; use function ipl\Stdlib\get_php_type; abstract class ObjectDetailExtensionHook extends BaseExtensionHook { /** * Load all extensions for the given object * * @param Host|Service|User|Usergroup|History $object * * @return array * * @throws InvalidArgumentException If the given model is not supported */ final public static function loadExtensions(Model $object): array { switch (true) { case $object instanceof Host: $hookName = 'Icingadb\\HostDetailExtension'; break; case $object instanceof Service: $hookName = 'Icingadb\\ServiceDetailExtension'; break; case $object instanceof RedundancyGroup: $hookName = 'Icingadb\\RedundancyGroupDetailExtension'; break; case $object instanceof User: $hookName = 'Icingadb\\UserDetailExtension'; break; case $object instanceof Usergroup: $hookName = 'Icingadb\\UsergroupDetailExtension'; break; case $object instanceof History: $hookName = 'Icingadb\\EventDetailExtension'; break; default: throw new InvalidArgumentException( sprintf('%s is not a supported object type', get_php_type($object)) ); } $extensions = []; $lastUsedLocations = []; /** * @var $hook HostDetailExtensionHook * @var $hook ServiceDetailExtensionHook * @var $hook UserDetailExtensionHook * @var $hook UsergroupDetailExtensionHook * @var $hook EventDetailExtensionHook */ foreach (Hook::all($hookName) as $hook) { $location = $hook->getLocation(); if ($location < 0) { $location = null; } if ($location === null) { $section = $hook->getSection(); if (! isset(self::BASE_LOCATIONS[$section])) { Logger::error('Detail extension %s is using an invalid section: %s', get_class($hook), $section); $section = self::DETAIL_SECTION; } if (isset($lastUsedLocations[$section])) { $location = ++$lastUsedLocations[$section]; } else { $location = self::BASE_LOCATIONS[$section]; $lastUsedLocations[$section] = $location; } } try { // It may be ValidHtml, but modules shouldn't be able to break our views. // That's why it needs to be rendered instantly, as any error will then // be caught here. $extension = (string) $hook->getHtmlForObject(clone $object); if (! empty($extension)) { $moduleName = $hook->getModule()->getName(); $extensions[$location] = new HtmlElement( 'div', Attributes::create([ 'class' => 'icinga-module module-' . $moduleName, 'data-icinga-module' => $moduleName ]), HtmlString::create($extension) ); } else { // There's no way for an extension to decide whether it should provide a result or not. // So, even if empty, the result must be used as it's perfectly valid. $extensions[$location] = HtmlString::create(''); } } catch (Throwable $e) { Logger::error("Failed to load detail extension: %s\n%s", $e, $e->getTraceAsString()); $extensions[$location] = Text::create(IcingaException::describe($e)); } } return $extensions; } } icingadb-web-1.4.0/library/Icingadb/Hook/ExtensionHook/ObjectsDetailExtensionHook.php000066400000000000000000000076001516124654200306440ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook\ExtensionHook; use Icinga\Application\Hook; use Icinga\Application\Logger; use Icinga\Exception\IcingaException; use Icinga\Module\Icingadb\Hook\HostsDetailExtensionHook; use Icinga\Module\Icingadb\Hook\ServicesDetailExtensionHook; use InvalidArgumentException; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Html\HtmlString; use ipl\Html\Text; use ipl\Html\ValidHtml; use ipl\Orm\Query; use ipl\Stdlib\BaseFilter; use ipl\Stdlib\Filter; use Throwable; abstract class ObjectsDetailExtensionHook extends BaseExtensionHook { use BaseFilter; /** * Load all extensions for the given objects * * @param string $objectType * @param Query $query * @param Filter\Rule $baseFilter * * @return array * * @throws InvalidArgumentException If the given object type is not supported */ final public static function loadExtensions(string $objectType, Query $query, Filter\Rule $baseFilter): array { switch ($objectType) { case 'host': $hookName = 'Icingadb\\HostsDetailExtension'; break; case 'service': $hookName = 'Icingadb\\ServicesDetailExtension'; break; default: throw new InvalidArgumentException( sprintf('%s is not a supported object type', $objectType) ); } $extensions = []; $lastUsedLocations = []; /** @var HostsDetailExtensionHook|ServicesDetailExtensionHook $hook */ foreach (Hook::all($hookName) as $hook) { $location = $hook->getLocation(); if ($location < 0) { $location = null; } if ($location === null) { $section = $hook->getSection(); if (! isset(self::BASE_LOCATIONS[$section])) { Logger::error('Detail extension %s is using an invalid section: %s', get_class($hook), $section); $section = self::DETAIL_SECTION; } if (isset($lastUsedLocations[$section])) { $location = ++$lastUsedLocations[$section]; } else { $location = self::BASE_LOCATIONS[$section]; $lastUsedLocations[$section] = $location; } } try { // It may be ValidHtml, but modules shouldn't be able to break our views. // That's why it needs to be rendered instantly, as any error will then // be caught here. $extension = (string) $hook->setBaseFilter($baseFilter)->getHtmlForObjects(clone $query); if (! empty($extension)) { $moduleName = $hook->getModule()->getName(); $extensions[$location] = new HtmlElement( 'div', Attributes::create([ 'class' => 'icinga-module module-' . $moduleName, 'data-icinga-module' => $moduleName ]), HtmlString::create($extension) ); } else { // There's no way for an extension to decide whether it should provide a result or not. // So, even if empty, the result must be used as it's perfectly valid. $extensions[$location] = HtmlString::create(''); } } catch (Throwable $e) { Logger::error("Failed to load details extension: %s\n%s", $e, $e->getTraceAsString()); $extensions[$location] = Text::create(IcingaException::describe($e)); } } return $extensions; } } icingadb-web-1.4.0/library/Icingadb/Hook/HostActionsHook.php000066400000000000000000000010621516124654200236700ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Module\Icingadb\Hook\ActionsHook\ObjectActionsHook; use Icinga\Module\Icingadb\Model\Host; use ipl\Web\Widget\Link; abstract class HostActionsHook extends ObjectActionsHook { /** * Assemble and return a list of HTML anchors for the given host * * @param Host $host * * @return Link[] */ abstract public function getActionsForObject(Host $host): array; } icingadb-web-1.4.0/library/Icingadb/Hook/HostDetailExtensionHook.php000066400000000000000000000011161516124654200253670ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\Host; use ipl\Html\ValidHtml; abstract class HostDetailExtensionHook extends ObjectDetailExtensionHook { /** * Assemble and return an HTML representation of the given host * * @param Host $host * * @return ValidHtml */ abstract public function getHtmlForObject(Host $host): ValidHtml; } icingadb-web-1.4.0/library/Icingadb/Hook/HostsDetailExtensionHook.php000066400000000000000000000017571516124654200255650ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectsDetailExtensionHook; use Icinga\Module\Icingadb\Model\Host; use ipl\Html\ValidHtml; use ipl\Orm\Query; abstract class HostsDetailExtensionHook extends ObjectsDetailExtensionHook { /** * Assemble and return an HTML representation of the given hosts * * The given query is already pre-filtered with the user's custom filter and restrictions. The base filter does * only contain the user's custom filter, use this for e.g. subsidiary links. * * The query is also limited by default, use `$hosts->limit(null)` to clear that. But beware that this may yield * a huge result set in case of a bulk selection. * * @param Query $hosts * * @return ValidHtml */ abstract public function getHtmlForObjects(Query $hosts): ValidHtml; } icingadb-web-1.4.0/library/Icingadb/Hook/IcingadbSupportHook.php000066400000000000000000000024061516124654200245320ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Application\Icinga; use Icinga\Authentication\Auth; use Icinga\Module\Icingadb\Hook\Common\HookUtils; use Icinga\Web\Session; abstract class IcingadbSupportHook { use HookUtils; /** * Return whether your module supports IcingaDB or not * * @return bool */ public function supportsIcingaDb(): bool { return true; } /** * Whether icingadb is set as the preferred backend in preferences * * @return bool Return true if icingadb is set as backend, false otherwise */ final public static function isIcingaDbSetAsPreferredBackend(): bool { return true; } /** * Whether to use icingadb as the backend * * @return bool Returns true if monitoring module is accessible or icingadb is selected as backend, false otherwise. */ final public static function useIcingaDbAsBackend(): bool { return ! Icinga::app()->getModuleManager()->hasEnabled('monitoring') || ! Auth::getInstance()->hasPermission('module/monitoring') || self::isIcingaDbSetAsPreferredBackend(); } } icingadb-web-1.4.0/library/Icingadb/Hook/PluginOutputHook.php000066400000000000000000000037071516124654200241210ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Application\Hook; use Icinga\Application\Logger; use Icinga\Module\Icingadb\Hook\Common\HookUtils; use Throwable; abstract class PluginOutputHook { use HookUtils; /** * Return whether the given command is supported or not * * @param string $commandName * * @return bool */ abstract public function isSupportedCommand(string $commandName): bool; /** * Process the given plugin output based on the specified check command * * Try to process the output as efficient and fast as possible. * Especially list view performance may suffer otherwise. * * @param string $output A host's or service's output * @param string $commandName The name of the checkcommand that produced the output * @param bool $enrichOutput Whether macros or other markup should be processed * * @return string */ abstract public function render(string $output, string $commandName, bool $enrichOutput): string; /** * Let all hooks process the given plugin output based on the specified check command * * @param string $output * @param string $commandName * @param bool $enrichOutput * * @return string */ final public static function processOutput(string $output, string $commandName, bool $enrichOutput): string { foreach (Hook::all('Icingadb\\PluginOutput') as $hook) { /** @var self $hook */ try { if ($hook->isSupportedCommand($commandName)) { $output = $hook->render($output, $commandName, $enrichOutput); } } catch (Throwable $e) { Logger::error("Unable to process plugin output: %s\n%s", $e, $e->getTraceAsString()); } } return $output; } } icingadb-web-1.4.0/library/Icingadb/Hook/RedundancyGroupDetailExtensionHook.php000066400000000000000000000012341516124654200275640ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\RedundancyGroup; use ipl\Html\ValidHtml; abstract class RedundancyGroupDetailExtensionHook extends ObjectDetailExtensionHook { /** * Assemble and return an HTML representation of the given redundancy group * * @param RedundancyGroup $redundancyGroup * * @return ValidHtml */ abstract public function getHtmlForObject(RedundancyGroup $redundancyGroup): ValidHtml; } icingadb-web-1.4.0/library/Icingadb/Hook/ServiceActionsHook.php000066400000000000000000000011071516124654200243530ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Module\Icingadb\Hook\ActionsHook\ObjectActionsHook; use Icinga\Module\Icingadb\Model\Service; use ipl\Web\Widget\Link; abstract class ServiceActionsHook extends ObjectActionsHook { /** * Assemble and return a list of HTML anchors for the given service * * @param Service $service * * @return Link[] */ abstract public function getActionsForObject(Service $service): array; } icingadb-web-1.4.0/library/Icingadb/Hook/ServiceDetailExtensionHook.php000066400000000000000000000011431516124654200260520ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\Service; use ipl\Html\ValidHtml; abstract class ServiceDetailExtensionHook extends ObjectDetailExtensionHook { /** * Assemble and return an HTML representation of the given service * * @param Service $service * * @return ValidHtml */ abstract public function getHtmlForObject(Service $service): ValidHtml; } icingadb-web-1.4.0/library/Icingadb/Hook/ServicesDetailExtensionHook.php000066400000000000000000000020011516124654200262270ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectsDetailExtensionHook; use Icinga\Module\Icingadb\Model\Service; use ipl\Html\ValidHtml; use ipl\Orm\Query; abstract class ServicesDetailExtensionHook extends ObjectsDetailExtensionHook { /** * Assemble and return an HTML representation of the given services * * The given query is already pre-filtered with the user's custom filter and restrictions. The base filter does * only contain the user's custom filter, use this for e.g. subsidiary links. * * The query is also limited by default, use `$hosts->limit(null)` to clear that. But beware that this may yield * a huge result set in case of a bulk selection. * * @param Query $services * * @return ValidHtml */ abstract public function getHtmlForObjects(Query $services): ValidHtml; } icingadb-web-1.4.0/library/Icingadb/Hook/TabHook.php000066400000000000000000000032571516124654200221500ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Hook\Common\HookUtils; use ipl\Html\ValidHtml; use ipl\Orm\Model; abstract class TabHook { use Auth; use Database; use HookUtils; /** * Get the tab's name * * The name is used to identify this hook later on. It must be unique. * Multiple words in the name should be separated by dashes. (-) * * @return string */ abstract public function getName(): string; /** * Get the tab's label * * The label is shown on the tab and in the browser's title. * * @return string */ abstract public function getLabel(): string; /** * Get tab content for the given object * * @param Model $object * * @return ValidHtml[] */ abstract public function getContent(Model $object): array; /** * Get tab controls for the given object * * @param Model $object * * @return ValidHtml[] */ public function getControls(Model $object): array { return []; } /** * Get tab footer for the given object * * @param Model $object * * @return ValidHtml[] */ public function getFooter(Model $object): array { return []; } /** * Get whether this tab should be shown * * @param Model $object * * @return bool */ public function shouldBeShown(Model $object): bool { return true; } } icingadb-web-1.4.0/library/Icingadb/Hook/TabHook/000077500000000000000000000000001516124654200214305ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Hook/TabHook/HookActions.php000066400000000000000000000076761516124654200244020ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook\TabHook; use Generator; use Icinga\Application\Hook; use Icinga\Application\Logger; use Icinga\Module\Icingadb\Hook\TabHook; use ipl\Html\ValidHtml; use ipl\Orm\Model; use ipl\Stdlib\Str; use Throwable; /** * Trait HookActions */ trait HookActions { /** @var Model The object to load tabs for */ protected $objectToLoadTabsFor; /** @var TabHook[] Loaded tab hooks */ protected $tabHooks; /** * Get default control elements * * @return ValidHtml[] */ abstract protected function getDefaultTabControls(): array; public function __call($methodName, $args) { if (substr($methodName, -6) === 'Action') { $hookName = substr($methodName, 0, -6); $hooks = $this->loadTabHooks(); if (isset($hooks[$hookName])) { $this->showTabHook($hooks[$hookName]); return; } } parent::__call($methodName, $args); } /** * Register the object for which to load additional tabs * * @param Model $object * * @return void */ protected function loadTabsForObject(Model $object) { $this->objectToLoadTabsFor = $object; } /** * Load tab hooks * * @return array */ protected function loadTabHooks(): array { if ($this->objectToLoadTabsFor === null) { return []; } elseif ($this->tabHooks !== null) { return $this->tabHooks; } $this->tabHooks = []; foreach (Hook::all('Icingadb\\Tab') as $hook) { /** @var TabHook $hook */ try { if ($hook->shouldBeShown($this->objectToLoadTabsFor)) { $this->tabHooks[Str::camel($hook->getName())] = $hook; } } catch (Throwable $e) { Logger::error("Failed to load tab hook: %s\n%s", $e, $e->getTraceAsString()); } } return $this->tabHooks; } /** * Load additional tabs * * @return Generator */ protected function loadAdditionalTabs(): Generator { foreach ($this->loadTabHooks() as $hook) { yield $hook->getName() => [ 'label' => $hook->getLabel(), 'url' => 'icingadb/' . $this->getRequest()->getControllerName() . '/' . $hook->getName() ]; } } /** * Render the given tab hook * * @param TabHook $hook * * @return void */ protected function showTabHook(TabHook $hook) { $moduleName = $hook->getModule()->getName(); foreach ($hook->getControls($this->objectToLoadTabsFor) as $control) { $this->addControl($control); } if (! empty($this->controls->getContent())) { $this->controls->addAttributes([ 'class' => ['icinga-module', 'module-' . $moduleName], 'data-icinga-module' => $moduleName ]); } else { foreach ($this->getDefaultTabControls() as $control) { $this->addControl($control); } } foreach ($hook->getContent($this->objectToLoadTabsFor) as $content) { $this->addContent($content); } $this->content->addAttributes([ 'class' => ['icinga-module', 'module-' . $moduleName], 'data-icinga-module' => $moduleName ]); foreach ($hook->getFooter($this->objectToLoadTabsFor) as $footer) { $this->addFooter($footer); } $this->footer->addAttributes([ 'class' => ['icinga-module', 'module-' . $moduleName], 'data-icinga-module' => $moduleName ]); } } icingadb-web-1.4.0/library/Icingadb/Hook/UserDetailExtensionHook.php000066400000000000000000000011161516124654200253700ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\User; use ipl\Html\ValidHtml; abstract class UserDetailExtensionHook extends ObjectDetailExtensionHook { /** * Assemble and return an HTML representation of the given user * * @param User $user * * @return ValidHtml */ abstract public function getHtmlForObject(User $user): ValidHtml; } icingadb-web-1.4.0/library/Icingadb/Hook/UsergroupDetailExtensionHook.php000066400000000000000000000011611516124654200264450ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Hook; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\Usergroup; use ipl\Html\ValidHtml; abstract class UsergroupDetailExtensionHook extends ObjectDetailExtensionHook { /** * Assemble and return an HTML representation of the given usergroup * * @param Usergroup $usergroup * * @return ValidHtml */ abstract public function getHtmlForObject(Usergroup $usergroup): ValidHtml; } icingadb-web-1.4.0/library/Icingadb/Model/000077500000000000000000000000001516124654200202415ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Model/AcknowledgementHistory.php000066400000000000000000000067611516124654200254550ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Behavior\BoolCast; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use ipl\Orm\Relations; /** * Model for table `acknowledgement_history` * * Please note that using this model will fetch history entries for decommissioned services. To avoid this, the query * needs a `acknowledgement_history.service_id IS NULL OR acknowledgement_history_service.id IS NOT NULL` where. * * @property string $id * @property string $environment_id * @property ?string $endpoint_id * @property string $object_type * @property string $host_id * @property ?string $service_id * @property DateTime $set_time * @property ?DateTime $clear_time * @property ?string $author * @property ?string $cleared_by * @property ?string $comment * @property ?DateTime $expire_time * @property ?bool $is_sticky * @property ?bool $is_persistent */ class AcknowledgementHistory extends Model { public function getTableName() { return 'acknowledgement_history'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'endpoint_id', 'object_type', 'host_id', 'service_id', 'set_time', 'clear_time', 'author', 'cleared_by', 'comment', 'expire_time', 'is_sticky', 'is_persistent' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'endpoint_id' => t('Endpoint Id'), 'object_type' => t('Object Type'), 'host_id' => t('Host Id'), 'service_id' => t('Service Id'), 'set_time' => t('Acknowledgement Set Time'), 'clear_time' => t('Acknowledgement Clear Time'), 'author' => t('Acknowledgement Author'), 'cleared_by' => t('Acknowledgement Cleared By'), 'comment' => t('Acknowledgement Comment'), 'expire_time' => t('Acknowledgement Expire Time'), 'is_sticky' => t('Acknowledgement Is Sticky'), 'is_persistent' => t('Acknowledgement Is Persistent') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new BoolCast([ 'is_sticky', 'is_persistent' ])); $behaviors->add(new MillisecondTimestamp([ 'set_time', 'clear_time', 'expire_time' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'endpoint_id', 'host_id', 'service_id', ])); } public function createRelations(Relations $relations) { $relations->belongsTo('endpoint', Endpoint::class); $relations->belongsTo('environment', Environment::class); $relations->belongsTo('history', History::class) ->setCandidateKey('id') ->setForeignKey('acknowledgement_history_id'); $relations->belongsTo('host', Host::class); $relations->belongsTo('service', Service::class)->setJoinType('LEFT'); } } icingadb-web-1.4.0/library/Icingadb/Model/ActionUrl.php000066400000000000000000000030451516124654200226540ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Common\Model; use Icinga\Module\Icingadb\Model\Behavior\ActionAndNoteUrl; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use ipl\Orm\Relations; /** * @property string $id * @property string[] $action_url * @property string $environment_id */ class ActionUrl extends Model { public function getTableName() { return 'action_url'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'action_url', 'environment_id' ]; } public function getColumnDefinitions() { return [ 'action_url' => t('Action Url'), 'environment_id' => t('Environment Id') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new ActionAndNoteUrl(['action_url'])); $behaviors->add(new Binary([ 'id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->hasMany('host', Host::class) ->setCandidateKey('id') ->setForeignKey('action_url_id'); $relations->hasMany('service', Service::class) ->setCandidateKey('id') ->setForeignKey('action_url_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/Behavior/000077500000000000000000000000001516124654200220005ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Model/Behavior/ActionAndNoteUrl.php000066400000000000000000000023211516124654200256600ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model\Behavior; use ipl\Orm\Contract\PropertyBehavior; class ActionAndNoteUrl extends PropertyBehavior { public function fromDb($value, $key, $_) { if (empty($value)) { return []; } $links = []; if (strpos($value, "' ") === false) { $links[] = $value; } else { foreach (explode("' ", $value) as $url) { $url = strpos($url, "'") === 0 ? substr($url, 1) : $url; $url = strpos($url, "'") === strlen($url) - 1 ? substr($url, 0, strlen($url) - 1) : $url; $links[] = $url; } } return $links; } public function toDb($value, $key, $_) { if (empty($value) || ! is_array($value)) { return $value; } if (count($value) === 1) { return $value[0]; } $links = ''; foreach ($value as $url) { if (! empty($links)) { $links .= ' '; } $links .= "'$url'"; } return $links; } } icingadb-web-1.4.0/library/Icingadb/Model/Behavior/Bitmask.php000066400000000000000000000044121516124654200241040ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model\Behavior; use ipl\Orm\Contract\PropertyBehavior; use ipl\Orm\Contract\RewriteFilterBehavior; use ipl\Stdlib\Filter\Condition; /** * Class Bitmask * * @method void __construct(array $properties) Pass property names as keys and their bitmap ([value => bit]) as value */ class Bitmask extends PropertyBehavior implements RewriteFilterBehavior { public function fromDb($bits, $key, $context) { $values = []; foreach ($context as $value => $bit) { if ($bits & $bit) { $values[] = $value; } } return $values; } public function toDb($value, $key, $context) { if (! is_array($value)) { if (is_int($value) || ctype_digit($value)) { return $value; } return isset($context[$value]) ? $context[$value] : -1; } $bits = []; $allBits = 0; foreach ($value as $v) { if (isset($context[$v])) { $bits[] = $context[$v]; $allBits |= $context[$v]; } elseif (is_int($v) || ctype_digit($v)) { $bits[] = $v; $allBits |= $v; } } $bits[] = $allBits; return $bits; } public function rewriteCondition(Condition $condition, $relation = null): null { $column = $condition->metaData()->get('columnName'); if ($column === null || ! isset($this->properties[$column])) { return null; } $values = $condition->getValue(); if (! is_array($values)) { if (is_int($values) || ctype_digit($values)) { return null; } $values = [$values]; } $bits = 0; foreach ($values as $value) { if (isset($this->properties[$column][$value])) { $bits |= $this->properties[$column][$value]; } elseif (is_int($value) || ctype_digit($value)) { $bits |= $value; } } $condition->setColumn(sprintf('%s & %s', $condition->getColumn(), $bits)); return null; } } icingadb-web-1.4.0/library/Icingadb/Model/Behavior/FlattenedObjectVars.php000066400000000000000000000117731516124654200264130ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model\Behavior; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Model\CustomvarFlat; use ipl\Orm\AliasedExpression; use ipl\Orm\ColumnDefinition; use ipl\Orm\Contract\QueryAwareBehavior; use ipl\Orm\Contract\RewriteColumnBehavior; use ipl\Orm\Query; use ipl\Stdlib\Filter; class FlattenedObjectVars implements RewriteColumnBehavior, QueryAwareBehavior { use Auth; /** @var string[] The labels to rewrite for column definition */ private const LABEL_REWRITE = [ 'user' => 'contact', 'usergroup' => 'contactgroup' ]; /** @var Query */ protected $query; public function setQuery(Query $query): static { $this->query = $query; return $this; } public function rewriteCondition(Filter\Condition $condition, $relation = null): ?Filter\Condition { $column = $condition->metaData()->get('columnName'); if ($column !== null) { $relation = substr($relation, 0, -5) . 'customvar_flat.'; $condition->metaData() ->set('requiresTransformation', true) ->set('columnPath', $relation . $column) ->set('relationPath', substr($relation, 0, -1)); if (isset($this->query->getWith()[substr($relation, 0, -1)])) { // In case customvar_flat is being selected, the FilterProcessor will not try to optimize the condition. // So we can prepare the condition here and still let CustomvarFlat's behavior do the rest. $condition->metaData()->set('columnName', $relation . $column); return $condition; } // The ORM's FilterProcessor only optimizes filter conditions that are in the same level (chain). // Previously, this behavior transformed a single condition to an ALL chain and hence the semantics // of the level changed, since the FilterProcessor interpreted the conditions separately from there on. // To not change the semantics of the condition it is required to delay the transformation of the condition // until the subquery is created. Though, since this is about custom variables, and such can contain dots, // the FilterProcessor then continues traversing the parts of the column's path, which then would include // the dot-separated parts of the custom variable name. To prevent this, we have to signal that what we // return a replacement here, that should be used as-is and not processed further. $condition->metaData()->set('forceResolved', true); // But to make it even worse: If we do that, (not transforming the condition) the FilterProcessor sees // multiple conditions as targeting different columns, as it doesn't know that the *columns* are in fact // custom variables. It then attempts to combine the conditions with an AND, which is not possible, since // they refer to the same columns (flatname and flatvalue) after being transformed. So we have to make // the condition refer to a different column, which is totally irrelevant, but since it's always the same // column, the FilterProcessor won't attempt to combine the conditions. The literal icing on the cake. $condition->setColumn('always_the_same_but_totally_irrelevant'); return $condition; } return null; } public function rewriteColumn($column, ?string $relation = null): AliasedExpression { $subQuery = $this->query->createSubQuery(new CustomvarFlat(), $relation) ->limit(1) ->columns('flatvalue') ->filter(Filter::equal('flatname', $column)); $this->applyRestrictions($subQuery); $alias = $this->query->getDb()->quoteIdentifier([str_replace('.', '_', $relation) . "_$column"]); list($select, $values) = $this->query->getDb()->getQueryBuilder()->assembleSelect($subQuery->assembleSelect()); return new AliasedExpression($alias, "($select)", null, ...$values); } public function rewriteColumnDefinition(ColumnDefinition $def, string $relation): void { $parts = explode('.', substr($relation, 0, -5)); $objectType = array_pop($parts); $name = $def->getName(); if (substr($name, -3) === '[*]') { // The suggestions also hide this from the label, so should this $name = substr($name, 0, -3); } if (array_key_exists($objectType, self::LABEL_REWRITE)) { $objectType = self::LABEL_REWRITE[$objectType]; } // Programmatically translated since the full definition is available in class ObjectSuggestions $def->setLabel(sprintf(t(ucfirst($objectType) . ' %s', '..'), $name)); } public function isSelectableColumn(string $name): bool { return true; } } icingadb-web-1.4.0/library/Icingadb/Model/Behavior/HasProblematicParent.php000066400000000000000000000057601516124654200265700ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model\Behavior; use Icinga\Module\Icingadb\Model\DependencyEdge; use ipl\Orm\AliasedExpression; use ipl\Orm\ColumnDefinition; use ipl\Orm\Exception\InvalidColumnException; use ipl\Orm\Query; use ipl\Sql\Expression; use ipl\Stdlib\Filter; use ipl\Orm\Contract\RewriteColumnBehavior; use ipl\Orm\Contract\QueryAwareBehavior; /** * Behavior to check if the service has a problematic parent */ class HasProblematicParent implements RewriteColumnBehavior, QueryAwareBehavior { /** @var Query */ protected $query; public function setQuery(Query $query): static { $this->query = $query; return $this; } public function rewriteColumn($column, ?string $relation = null): ?AliasedExpression { if (! $this->isSelectableColumn($column)) { return null; } $resolver = $this->query->getResolver(); if ($relation !== null) { $serviceTableAlias = $resolver->getAlias($resolver->resolveRelation($relation)->getTarget()); $column = $resolver->qualifyColumnAlias($column, $serviceTableAlias); } else { $serviceTableAlias = $resolver->getAlias($this->query->getModel()); } $subQueryModel = new DependencyEdge(); $subQuery = (new Query()) ->setDb($this->query->getDb()) ->setModel($subQueryModel) ->columns([new Expression('1')]) ->utilize('from') ->limit(1) ->filter(Filter::equal('state.failed', 'y')); $subQueryResolver = $subQuery->getResolver()->setAliasPrefix('hpp_'); $subQueryTarget = $subQueryResolver->resolveRelation($subQueryModel->getTableName() . '.from')->getTarget(); $targetForeignKey = $subQueryResolver->qualifyColumn( 'service_id', $subQueryResolver->getAlias($subQueryTarget) ); $subQuery->getSelectBase() ->where("$targetForeignKey = {$resolver->qualifyColumn('id', $serviceTableAlias)}"); [$select, $values] = $this->query->getDb() ->getQueryBuilder() ->assembleSelect($subQuery->assembleSelect()); return new AliasedExpression( $this->query->getDb()->quoteIdentifier([$column]), "($select)", null, ...$values ); } public function isSelectableColumn(string $name): bool { return $name === 'has_problematic_parent'; } public function rewriteColumnDefinition(ColumnDefinition $def, string $relation): void { } public function rewriteCondition(Filter\Condition $condition, $relation = null): null { $column = substr($condition->getColumn(), strlen($relation ?? '')); if ($this->isSelectableColumn($column)) { throw new InvalidColumnException($column, $this->query->getModel()); } return null; } } icingadb-web-1.4.0/library/Icingadb/Model/Behavior/ReRoute.php000066400000000000000000000050151516124654200240770ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model\Behavior; use ipl\Orm\Contract\RewriteFilterBehavior; use ipl\Orm\Contract\RewritePathBehavior; use ipl\Stdlib\Filter; class ReRoute implements RewriteFilterBehavior, RewritePathBehavior { protected $routes; /** * Tables with mixed object type entries for which servicegroup filters need to be resolved in multiple steps * * @var string[] */ public const MIXED_TYPE_RELATIONS = ['downtime', 'comment', 'history', 'notification_history']; public function __construct(array $routes) { $this->routes = $routes; } public function getRoutes(): array { return $this->routes; } public function rewriteCondition(Filter\Condition $condition, $relation = null): ?Filter\Rule { $remainingPath = $condition->metaData()->get('columnName', ''); if (strpos($remainingPath, '.') === false) { return null; } if (($path = $this->rewritePath($remainingPath, $relation)) !== null) { $class = get_class($condition); $filter = new $class($relation . $path, $condition->getValue()); if ($condition->metaData()->has('forceOptimization')) { $filter->metaData()->set( 'forceOptimization', $condition->metaData()->get('forceOptimization') ); } if ( in_array(substr($relation, 0, -1), self::MIXED_TYPE_RELATIONS) && substr($remainingPath, 0, 13) === 'servicegroup.' ) { $applyAll = Filter::all(); $applyAll->add(Filter::equal($relation . 'object_type', 'host')); $orgFilter = clone $filter; $orgFilter->setColumn($relation . 'host.' . $path); $applyAll->add($orgFilter); $filter = Filter::any($filter, $applyAll); } return $filter; } return null; } public function rewritePath(string $path, ?string $relation = null): ?string { $dot = strpos($path, '.'); if ($dot !== false) { $routeName = substr($path, 0, $dot); } else { $routeName = $path; } if (isset($this->routes[$routeName])) { return $this->routes[$routeName] . ($dot !== false ? substr($path, $dot) : ''); } return null; } } icingadb-web-1.4.0/library/Icingadb/Model/Checkcommand.php000066400000000000000000000055571516124654200233420ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Common\Model; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use ipl\Orm\Relations; /** * @property string $id * @property ?string $zone_id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $command * @property int $timeout */ class Checkcommand extends Model { public function getTableName() { return 'checkcommand'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'zone_id', 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'command', 'timeout' ]; } public function getColumnDefinitions() { return [ 'zone_id' => t('Zone Id'), 'environment_id' => t('Environment Id'), 'name_checksum' => t('Checkcommand Name Checksum'), 'properties_checksum' => t('Checkcommand Properties Checksum'), 'name' => t('Checkcommand Name'), 'name_ci' => t('Checkcommand Name (CI)'), 'command' => t('Checkcommand'), 'timeout' => t('Checkcommand Timeout') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new ReRoute([ 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); $behaviors->add(new Binary([ 'id', 'zone_id', 'environment_id', 'name_checksum', 'properties_checksum' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->belongsToMany('customvar', Customvar::class) ->through(CheckcommandCustomvar::class) ->setThroughAlias('t_checkcommand_customvar'); $relations->belongsToMany('customvar_flat', CustomvarFlat::class) ->through(CheckcommandCustomvar::class); $relations->belongsToMany('vars', Vars::class) ->through(CheckcommandCustomvar::class); $relations->hasMany('argument', CheckcommandArgument::class); $relations->hasMany('envvar', CheckcommandEnvvar::class); $relations->hasMany('host', Host::class); $relations->hasMany('service', Service::class); } } icingadb-web-1.4.0/library/Icingadb/Model/CheckcommandArgument.php000066400000000000000000000054011516124654200250310ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use ipl\Orm\Relations; /** * @property string $id * @property string $checkcommand_id * @property string $argument_key * @property string $environment_id * @property string $properties_checksum * @property ?string $argument_value * @property ?int $argument_order * @property ?string $description * @property ?string $argument_key_override * @property string $repeat_key * @property string $required * @property ?string $set_if * @property ?string $separator * @property string $skip_key */ class CheckcommandArgument extends Model { public function getTableName() { return 'checkcommand_argument'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'checkcommand_id', 'argument_key', 'environment_id', 'properties_checksum', 'argument_value', 'argument_order', 'description', 'argument_key_override', 'repeat_key', 'required', 'set_if', 'separator', 'skip_key' ]; } public function getColumnDefinitions() { return [ 'checkcommand_id' => t('Checkcommand Id'), 'argument_key' => t('Checkcommand Argument Name'), 'environment_id' => t('Environment Id'), 'properties_checksum' => t('Checkcommand Argument Properties Checksum'), 'argument_value' => t('Checkcommand Argument Value'), 'argument_order' => t('Checkcommand Argument Position'), 'description' => t('Checkcommand Argument Description'), 'argument_key_override' => t('Checkcommand Argument Actual Name'), 'repeat_key' => t('Checkcommand Argument Repeated'), 'required' => t('Checkcommand Argument Required'), 'set_if' => t('Checkcommand Argument Condition'), 'skip_key' => t('Checkcommand Argument Without Name') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'checkcommand_id', 'environment_id', 'properties_checksum' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('checkcommand', Checkcommand::class); } } icingadb-web-1.4.0/library/Icingadb/Model/CheckcommandCustomvar.php000066400000000000000000000026311516124654200252340ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use ipl\Orm\Relations; /** * @property string $id * @property string $checkcommand_id * @property string $customvar_id * @property string $environment_id */ class CheckcommandCustomvar extends Model { public function getTableName() { return 'checkcommand_customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'checkcommand_id', 'customvar_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'checkcommand_id', 'customvar_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('checkcommand', Checkcommand::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsTo('customvar_flat', CustomvarFlat::class) ->setCandidateKey('customvar_id') ->setForeignKey('customvar_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/CheckcommandEnvvar.php000066400000000000000000000033501516124654200245110ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $checkcommand_id * @property string $envvar_key * @property string $environment_id * @property string $properties_checksum * @property string $envvar_value */ class CheckcommandEnvvar extends Model { public function getTableName() { return 'checkcommand_envvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'checkcommand_id', 'envvar_key', 'environment_id', 'properties_checksum', 'envvar_value' ]; } public function getColumnDefinitions() { return [ 'checkcommand_id' => t('Checkcommand Id'), 'envvar_key' => t('Checkcommand Envvar Name'), 'environment_id' => t('Environment Id'), 'properties_checksum' => t('Checkcommand Properties Checksum'), 'envvar_value' => t('Checkcommand Envvar Value') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'checkcommand_id', 'environment_id', 'properties_checksum' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('checkcommand', Checkcommand::class); } } icingadb-web-1.4.0/library/Icingadb/Model/Comment.php000066400000000000000000000072441516124654200223630ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use ipl\Orm\Behavior\BoolCast; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property string $object_type * @property string $host_id * @property ?string $service_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $author * @property string $text * @property string $entry_type * @property DateTime $entry_time * @property bool $is_persistent * @property bool $is_sticky * @property ?DateTime $expire_time * @property ?string $zone_id */ class Comment extends Model { public function getTableName() { return 'comment'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'object_type', 'host_id', 'service_id', 'name_checksum', 'properties_checksum', 'name', 'author', 'text', 'entry_type', 'entry_time', 'is_persistent', 'is_sticky', 'expire_time', 'zone_id' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'object_type' => t('Object Type'), 'host_id' => t('Host Id'), 'service_id' => t('Service Id'), 'name_checksum' => t('Comment Name Checksum'), 'properties_checksum' => t('Comment Properties Checksum'), 'name' => t('Comment Name'), 'author' => t('Comment Author'), 'text' => t('Comment Text'), 'entry_type' => t('Comment Type'), 'entry_time' => t('Comment Entry Time'), 'is_persistent' => t('Comment Is Persistent'), 'is_sticky' => t('Comment Is Sticky'), 'expire_time' => t('Comment Expire Time'), 'zone_id' => t('Zone Id') ]; } public function getSearchColumns() { return ['text']; } public function getDefaultSort() { return 'comment.entry_time desc'; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new BoolCast([ 'is_persistent', 'is_sticky' ])); $behaviors->add(new MillisecondTimestamp([ 'entry_time', 'expire_time' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'host_id', 'service_id', 'name_checksum', 'properties_checksum', 'zone_id' ])); $behaviors->add(new ReRoute([ 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('host', Host::class)->setJoinType('LEFT'); $relations->belongsTo('service', Service::class)->setJoinType('LEFT'); $relations->belongsTo('zone', Zone::class); } } icingadb-web-1.4.0/library/Icingadb/Model/CommentHistory.php000066400000000000000000000073011516124654200237370ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use ipl\Orm\Behavior\BoolCast; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * Model for table `comment_history` * * Please note that using this model will fetch history entries for decommissioned services. To avoid this, * the query needs a `comment_history.service_id IS NULL OR comment_history_service.id IS NOT NULL` where. * * @property string $comment_id * @property string $environment_id * @property ?string $endpoint_id * @property string $object_type * @property string $host_id * @property ?string $service_id * @property DateTime $entry_time * @property string $author * @property ?string $removed_by * @property string $comment * @property string $entry_type * @property bool $is_persistent * @property bool $is_sticky * @property ?DateTime $expire_time * @property ?DateTime $remove_time * @property bool $has_been_removed */ class CommentHistory extends Model { public function getTableName() { return 'comment_history'; } public function getKeyName() { return 'comment_id'; } public function getColumns() { return [ 'environment_id', 'endpoint_id', 'object_type', 'host_id', 'service_id', 'entry_time', 'author', 'removed_by', 'comment', 'entry_type', 'is_persistent', 'is_sticky', 'expire_time', 'remove_time', 'has_been_removed' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'endpoint_id' => t('Endpoint Id'), 'object_type' => t('Object Type'), 'host_id' => t('Host Id'), 'service_id' => t('Service Id'), 'entry_time' => t('Comment Entry Time'), 'author' => t('Comment Author'), 'removed_by' => t('Comment Removed By'), 'comment' => t('Comment Text'), 'entry_type' => t('Comment Entry Type'), 'is_persistent' => t('Comment Is Persistent'), 'is_sticky' => t('Comment Is Sticky'), 'expire_time' => t('Comment Expire Time'), 'remove_time' => t('Comment Remove Time'), 'has_been_removed' => t('Comment Has Been Removed') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new BoolCast([ 'is_persistent', 'is_sticky', 'has_been_removed' ])); $behaviors->add(new MillisecondTimestamp([ 'entry_time', 'expire_time', 'remove_time' ])); $behaviors->add(new Binary([ 'comment_id', 'environment_id', 'endpoint_id', 'host_id', 'service_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('endpoint', Endpoint::class); $relations->belongsTo('environment', Environment::class); $relations->belongsTo('history', History::class) ->setCandidateKey('comment_id') ->setForeignKey('comment_history_id'); $relations->belongsTo('host', Host::class); $relations->belongsTo('service', Service::class)->setJoinType('LEFT'); } } icingadb-web-1.4.0/library/Icingadb/Model/Customvar.php000066400000000000000000000046641516124654200227470ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property string $name_checksum * @property string $name * @property string $value */ class Customvar extends Model { public function getTableName() { return 'customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'name_checksum', 'name', 'value' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'environment_id', 'name_checksum' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsToMany('checkcommand', Checkcommand::class) ->through(CheckcommandCustomvar::class); $relations->belongsToMany('eventcommand', Eventcommand::class) ->through(EventcommandCustomvar::class); $relations->belongsToMany('host', Host::class) ->through(HostCustomvar::class); $relations->belongsToMany('hostgroup', Hostgroup::class) ->through(HostgroupCustomvar::class); $relations->belongsToMany('notification', Notification::class) ->through(NotificationCustomvar::class); $relations->belongsToMany('notificationcommand', Notificationcommand::class) ->through(NotificationcommandCustomvar::class); $relations->belongsToMany('service', Service::class) ->through(ServiceCustomvar::class); $relations->belongsToMany('servicegroup', Servicegroup::class) ->through(ServicegroupCustomvar::class); $relations->belongsToMany('timeperiod', Timeperiod::class) ->through(TimeperiodCustomvar::class); $relations->belongsToMany('user', User::class) ->through(UserCustomvar::class); $relations->belongsToMany('usergroup', Usergroup::class) ->through(UsergroupCustomvar::class); $relations->hasMany('customvar_flat', CustomvarFlat::class); } } icingadb-web-1.4.0/library/Icingadb/Model/CustomvarFlat.php000066400000000000000000000164751516124654200235610ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use ipl\Orm\Contract\RewriteFilterBehavior; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Query; use ipl\Orm\Relations; use ipl\Stdlib\Filter; use ipl\Stdlib\Filter\Condition; use Traversable; /** * @property string $id * @property string $environment_id * @property string $customvar_id * @property string $flatname_checksum * @property string $flatname * @property ?string $flatvalue */ class CustomvarFlat extends Model { public function getTableName() { return 'customvar_flat'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'customvar_id', 'flatname_checksum', 'flatname', 'flatvalue' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'environment_id', 'customvar_id', 'flatname_checksum' ])); $behaviors->add(new class implements RewriteFilterBehavior { public function rewriteCondition(Condition $condition, $relation = null): ?Filter\Chain { if ($condition->metaData()->has('requiresTransformation')) { /** @var string $columnName */ $columnName = $condition->metaData()->get('columnName'); $nameFilter = Filter::like($relation . 'flatname', $columnName); $class = get_class($condition); $valueFilter = new $class($relation . 'flatvalue', $condition->getValue()); return Filter::all($nameFilter, $valueFilter); } return null; } }); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsToMany('checkcommand', Checkcommand::class) ->through(CheckcommandCustomvar::class) ->setThroughAlias('t_checkcommand_customvar') ->setCandidateKey('customvar_id'); $relations->belongsToMany('eventcommand', Eventcommand::class) ->through(EventcommandCustomvar::class) ->setThroughAlias('t_eventcommand_customvar') ->setCandidateKey('customvar_id'); $relations->belongsToMany('host', Host::class) ->through(HostCustomvar::class) ->setThroughAlias('t_host_customvar') ->setCandidateKey('customvar_id'); $relations->belongsToMany('hostgroup', Hostgroup::class) ->through(HostgroupCustomvar::class) ->setThroughAlias('t_hostgroup_customvar') ->setCandidateKey('customvar_id'); $relations->belongsToMany('notification', Notification::class) ->through(NotificationCustomvar::class) ->setThroughAlias('t_notification_customvar') ->setCandidateKey('customvar_id'); $relations->belongsToMany('notificationcommand', Notificationcommand::class) ->through(NotificationcommandCustomvar::class) ->setThroughAlias('t_notificationcommand_customvar') ->setCandidateKey('customvar_id'); $relations->belongsToMany('service', Service::class) ->through(ServiceCustomvar::class) ->setThroughAlias('t_service_customvar') ->setCandidateKey('customvar_id'); $relations->belongsToMany('servicegroup', Servicegroup::class) ->through(ServicegroupCustomvar::class) ->setThroughAlias('t_servicegroup_customvar') ->setCandidateKey('customvar_id'); $relations->belongsToMany('timeperiod', Timeperiod::class) ->through(TimeperiodCustomvar::class) ->setThroughAlias('t_timeperiod_customvar') ->setCandidateKey('customvar_id'); $relations->belongsToMany('user', User::class) ->through(UserCustomvar::class) ->setThroughAlias('t_user_customvar') ->setCandidateKey('customvar_id'); $relations->belongsToMany('usergroup', Usergroup::class) ->through(UsergroupCustomvar::class) ->setThroughAlias('t_usergroup_customvar') ->setCandidateKey('customvar_id'); } /** * Restore flattened custom variables to their previous structure * * @param Traversable $flattenedVars * * @return array */ public function unFlattenVars(Traversable $flattenedVars): array { $registerValue = function (&$data, $source, $path, $value) use (&$registerValue) { $step = array_shift($path); $isIndex = (bool) preg_match('/^\[(\d+)]$/', $step, $m); if ($isIndex) { $step = $m[1]; } if ($source !== null) { while (! isset($source[$step])) { if ($isIndex) { $step = sprintf('[%d]', $step); $isIndex = false; } else { if (empty($path)) { break; } $step = implode('.', [$step, array_shift($path)]); } } } if (! empty($path)) { if (! isset($data[$step])) { $data[$step] = []; } $registerValue($data[$step], $source[$step] ?? null, $path, $value); } else { // Since empty custom vars of type dictionaries and arrays have null values in customvar_flat table, // we won't be able to render them as such. Therefore, we have to use the value of the `customvar` // table if it's not null, otherwise the current value, which is a "null" string. $data[$step] = $value === null && ($source[$step] ?? null) === [] ? $source[$step] : $value; } }; if ($flattenedVars instanceof Query) { $flattenedVars->withColumns(['customvar.name', 'customvar.value']); } $vars = []; foreach ($flattenedVars as $var) { if (isset($var->customvar->name)) { $var->customvar->value = json_decode($var->customvar->value, true); $realName = $var->customvar->name; $source = [$realName => $var->customvar->value]; $sourcePath = ltrim(substr($var->flatname, strlen($realName)), '.'); $path = array_merge( [$realName], $sourcePath ? preg_split('/(?<=\w|])\.|(?flatname); $source = null; } $registerValue($vars, $source, $path, $var->flatvalue); if (isset($var->customvar->name)) { $var->customvar->name = null; $var->customvar->value = null; } } return $vars; } } icingadb-web-1.4.0/library/Icingadb/Model/DependencyEdge.php000066400000000000000000000040721516124654200236200ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Query; use ipl\Orm\Relations; /** * Dependency edge model. * * @property string $id * @property string $environment_id * @property string $to_node_id * @property string $from_node_id * @property string $display_name * @property string $dependency_edge_state_id * * @property DependencyNode|Query $child * @property DependencyNode|Query $parent * @property DependencyEdgeState|Query $state */ class DependencyEdge extends Model { public function getTableName(): string { return 'dependency_edge'; } public function getKeyName(): string { return 'id'; } public function getColumns(): array { return [ 'environment_id', 'to_node_id', 'from_node_id', 'display_name', 'dependency_edge_state_id' ]; } public function createBehaviors(Behaviors $behaviors): void { $behaviors->add(new Binary([ 'id', 'environment_id', 'to_node_id', 'from_node_id', 'dependency_edge_state_id' ])); } public function createRelations(Relations $relations): void { $relations->belongsTo('child', DependencyNode::class) ->setCandidateKey('from_node_id'); $relations->belongsTo('parent', DependencyNode::class) ->setCandidateKey('to_node_id'); $relations->hasOne('state', DependencyEdgeState::class) ->setCandidateKey('dependency_edge_state_id') ->setForeignKey('id'); // "from" and "to" are only necessary for sub-query filters. $relations->belongsTo('from', DependencyNode::class) ->setCandidateKey('from_node_id'); $relations->belongsTo('to', DependencyNode::class) ->setCandidateKey('to_node_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/DependencyEdgeState.php000066400000000000000000000022101516124654200246110ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\BoolCast; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * Dependency edge state model. * * @property string $id * @property string $environment_id * @property bool $failed */ class DependencyEdgeState extends Model { public function getTableName(): string { return 'dependency_edge_state'; } public function getKeyName(): string { return 'id'; } public function getColumns(): array { return [ 'environment_id', 'failed' ]; } public function createBehaviors(Behaviors $behaviors): void { $behaviors->add(new Binary([ 'id', 'environment_id' ])); $behaviors->add(new BoolCast([ 'failed' ])); } public function createRelations(Relations $relations) { $relations->hasMany('edge', DependencyEdge::class); } } icingadb-web-1.4.0/library/Icingadb/Model/DependencyNode.php000066400000000000000000000142141516124654200236400ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Query; use ipl\Orm\Relations; use ipl\Sql\Connection; use ipl\Sql\Expression; use ipl\Sql\Filter\Exists; use ipl\Stdlib\Filter; /** * Dependency node model. * * @property string $id * @property string $environment_id * @property ?string $host_id * @property ?string $service_id * @property ?string $redundancy_group_id * @property string $name * @property string $severity * @property string $state * @property string $last_state_change * * @property (?Host)|Query $host * @property (?Service)|Query $service * @property (?RedundancyGroup)|Query $redundancy_group * @property (?DependencyEdge)|Query $from * @property (?DependencyEdge)|Query $to */ class DependencyNode extends Model { public function getTableName(): string { return 'dependency_node'; } public function getKeyName(): string { return 'id'; } public function getColumns(): array { return [ 'id', 'environment_id', 'host_id', 'service_id', 'redundancy_group_id', 'name' => new Expression( 'COALESCE(%s, %s, %s)', ['service.display_name', 'host.display_name', 'redundancy_group.display_name'] ), 'severity' => new Expression( "COALESCE(%s, %s, CASE WHEN %s = 'y' THEN 1 ELSE 0 END)", ['service.state.severity', 'host.state.severity', 'redundancy_group.state.failed'] ), 'state' => new Expression( "COALESCE(%s, %s, CASE WHEN %s = 'y' THEN 1 ELSE 0 END)", ['service.state.soft_state', 'host.state.soft_state', 'redundancy_group.state.failed'] ), 'last_state_change' => new Expression( 'COALESCE(%s, %s, %s)', [ 'service.state.last_state_change', 'host.state.last_state_change', 'redundancy_group.state.last_state_change' ] ), ]; } public function getSearchColumns(): array { return [ 'host.name_ci', 'service.name_ci', 'redundancy_group.display_name' ]; } public function getDefaultSort(): array { return ['severity DESC', 'last_state_change DESC']; } public function createBehaviors(Behaviors $behaviors): void { $behaviors->add(new Binary([ 'id', 'environment_id', 'host_id', 'service_id', 'redundancy_group_id' ])); $behaviors->add(new ReRoute([ 'child' => 'to.from', 'parent' => 'from.to', 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); } public function createRelations(Relations $relations): void { $relations->belongsTo('host', Host::class) ->setJoinType('LEFT'); $relations->belongsTo('service', Service::class) ->setJoinType('LEFT'); $relations->belongsTo('redundancy_group', RedundancyGroup::class) ->setJoinType('LEFT'); $relations->hasMany('from', DependencyEdge::class) ->setForeignKey('from_node_id') ->setJoinType('LEFT'); $relations->hasMany('to', DependencyEdge::class) ->setForeignKey('to_node_id') ->setJoinType('LEFT'); // TODO: This self join is only a work-around as when selecting nodes and filtering by child or parent, // the ORM wants to join the base table as usual in case a sub-query is used. Though, in this case // resolving e.g. child to "to.from" is reversed in a sub-query to "from.to" and the ORM does not // detect that "to" is already the link to the base table. // Given the path "dependency_node.to.from.host", the sub-query uses "host.from.to.dependency_node". // "to.dependency_node" is the crucial part, as "dependency_node" is said self-join. $relations->hasOne('dependency_node', self::class) ->setForeignKey('id'); } /** * Get the query with only (direct) parents or children of the given host id. * * @internal Bug: This fix is required for host based queries. Otherwise, ipl-orm makes two subqueries * from the filter, which leads to incorrect results. * * @todo This is a workaround, remove it once https://github.com/Icinga/ipl-orm/issues/119 is fixed * * @param string $hostId Host id to fetch parents or children for * @param Connection $db The database connection * @param bool $fetchParents Fetch parents if true, children otherwise * * @return Query */ public static function forHost(string $hostId, Connection $db, bool $fetchParents = false): Query { $filterTable = $fetchParents ? 'child' : 'parent'; $utilizeType = $fetchParents ? 'parent' : 'child'; $edge = DependencyEdge::on($db) ->utilize($utilizeType) ->columns([new Expression('1')]) ->filter(Filter::all( Filter::equal("$filterTable.host.id", $hostId), Filter::unlike("$filterTable.service.id", '*') )); $edge->getFilter()->metaData()->set('forceOptimization', false); $resolver = $edge->getResolver(); $edgeAlias = $resolver->getAlias( $resolver->resolveRelation($resolver->qualifyPath($utilizeType, $edge->getModel()->getTableName())) ->getTarget() ); $query = static::on($db); $query->filter(new Exists( $edge->assembleSelect() ->where( "$edgeAlias.id = " . $query->getResolver()->qualifyColumn('id', $query->getModel()->getTableName()) ) )); return $query; } } icingadb-web-1.4.0/library/Icingadb/Model/Downtime.php000066400000000000000000000126401516124654200225430ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use ipl\Orm\Behavior\BoolCast; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property ?string $triggered_by_id * @property ?string $parent_id * @property string $object_type * @property string $host_id * @property ?string $service_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $author * @property string $comment * @property DateTime $entry_time * @property DateTime $scheduled_start_time * @property DateTime $scheduled_end_time * @property int $scheduled_duration * @property bool $is_flexible * @property int $flexible_duration * @property bool $is_in_effect * @property ?DateTime $start_time * @property ?DateTime $end_time * @property int $duration * @property ?string $scheduled_by * @property ?string $zone_id */ class Downtime extends Model { public function getTableName() { return 'downtime'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'triggered_by_id', 'parent_id', 'object_type', 'host_id', 'service_id', 'name_checksum', 'properties_checksum', 'name', 'author', 'comment', 'entry_time', 'scheduled_start_time', 'scheduled_end_time', 'scheduled_duration', 'is_flexible', 'flexible_duration', 'is_in_effect', 'start_time', 'end_time', 'duration', 'scheduled_by', 'zone_id' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'triggered_by_id' => t('Triggered By Downtime Id'), 'parent_id' => t('Parent Downtime Id'), 'object_type' => t('Object Type'), 'host_id' => t('Host Id'), 'service_id' => t('Service Id'), 'name_checksum' => t('Downtime Name Checksum'), 'properties_checksum' => t('Downtime Properties Checksum'), 'name' => t('Downtime Name'), 'author' => t('Downtime Author'), 'comment' => t('Downtime Comment'), 'entry_time' => t('Downtime Entry Time'), 'scheduled_start_time' => t('Downtime Scheduled Start'), 'scheduled_end_time' => t('Downtime Scheduled End'), 'scheduled_duration' => t('Downtime Scheduled Duration'), 'is_flexible' => t('Downtime Is Flexible'), 'flexible_duration' => t('Downtime Flexible Duration'), 'is_in_effect' => t('Downtime Is In Effect'), 'start_time' => t('Downtime Actual Start'), 'end_time' => t('Downtime Actual End'), 'duration' => t('Downtime Duration'), 'scheduled_by' => t('Scheduled By Downtime'), 'zone_id' => t('Zone Id') ]; } public function getSearchColumns() { return ['comment']; } public function getDefaultSort() { return ['downtime.is_in_effect desc', 'downtime.start_time desc']; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new BoolCast([ 'is_flexible', 'is_in_effect' ])); $behaviors->add(new MillisecondTimestamp([ 'entry_time', 'scheduled_start_time', 'scheduled_end_time', 'start_time', 'end_time' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'triggered_by_id', 'parent_id', 'host_id', 'service_id', 'name_checksum', 'properties_checksum', 'zone_id' ])); // As long as the rewriteCondition() expects only Filter\Condition as a first argument // We have to add this reroute behavior after the binary because the filter condition might // be transformed into a filter chain! $behaviors->add(new ReRoute([ 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('triggered_by', self::class) ->setCandidateKey('triggered_by_id') ->setJoinType('LEFT'); $relations->belongsTo('parent', self::class) ->setCandidateKey('parent_id') ->setJoinType('LEFT'); $relations->belongsTo('environment', Environment::class); $relations->belongsTo('host', Host::class)->setJoinType('LEFT'); $relations->belongsTo('service', Service::class)->setJoinType('LEFT'); $relations->belongsTo('zone', Zone::class); } } icingadb-web-1.4.0/library/Icingadb/Model/DowntimeHistory.php000066400000000000000000000117171516124654200241310ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use ipl\Orm\Behavior\BoolCast; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * Model for table `downtime_history` * * Please note that using this model will fetch history entries for decommissioned services. To avoid this, * the query needs a `downtime_history.service_id IS NULL OR downtime_history_service.id IS NOT NULL` where. * * @property string $id * @property string $environment_id * @property ?string $endpoint_id * @property ?string $triggered_by_id * @property ?string $parent_id * @property string $object_type * @property string $host_id * @property ?string $service_id * @property DateTime $entry_time * @property string $author * @property ?string $cancelled_by * @property string $comment * @property bool $is_flexible * @property int $flexible_duration * @property DateTime $scheduled_start_time * @property DateTime $scheduled_end_time * @property DateTime $start_time * @property DateTime $end_time * @property ?string $scheduled_by * @property bool $has_been_cancelled * @property DateTime $trigger_time * @property ?DateTime $cancel_time */ class DowntimeHistory extends Model { public function getTableName() { return 'downtime_history'; } public function getKeyName() { return 'downtime_id'; } public function getColumns() { return [ 'environment_id', 'endpoint_id', 'triggered_by_id', 'parent_id', 'object_type', 'host_id', 'service_id', 'entry_time', 'author', 'cancelled_by', 'comment', 'is_flexible', 'flexible_duration', 'scheduled_start_time', 'scheduled_end_time', 'start_time', 'end_time', 'scheduled_by', 'has_been_cancelled', 'trigger_time', 'cancel_time' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'endpoint_id' => t('Endpoint Id'), 'triggered_by_id' => t('Triggered By Downtime Id'), 'parent_id' => t('Parent Downtime Id'), 'object_type' => t('Object Type'), 'host_id' => t('Host Id'), 'service_id' => t('Service Id'), 'entry_time' => t('Downtime Entry Time'), 'author' => t('Downtime Author'), 'cancelled_by' => t('Downtime Cancelled By'), 'comment' => t('Downtime Comment'), 'is_flexible' => t('Downtime Is Flexible'), 'flexible_duration' => t('Downtime Flexible Duration'), 'scheduled_start_time' => t('Downtime Scheduled Start'), 'scheduled_end_time' => t('Downtime Scheduled End'), 'start_time' => t('Downtime Actual Start'), 'end_time' => t('Downtime Actual End'), 'has_been_cancelled' => t('Downtime Has Been Cancelled'), 'trigger_time' => t('Downtime Trigger Time'), 'cancel_time' => t('Downtime Cancel Time') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new BoolCast([ 'is_flexible', 'has_been_cancelled' ])); $behaviors->add(new MillisecondTimestamp([ 'entry_time', 'scheduled_start_time', 'scheduled_end_time', 'start_time', 'end_time', 'trigger_time', 'cancel_time' ])); $behaviors->add(new Binary([ 'downtime_id', 'environment_id', 'endpoint_id', 'triggered_by_id', 'parent_id', 'host_id', 'service_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('triggered_by', self::class) ->setCandidateKey('triggered_by_id') ->setJoinType('LEFT'); $relations->belongsTo('parent', self::class) ->setCandidateKey('parent_id') ->setJoinType('LEFT'); $relations->belongsTo('endpoint', Endpoint::class); $relations->belongsTo('environment', Environment::class); $relations->belongsTo('history', History::class) ->setCandidateKey('downtime_id') ->setForeignKey('downtime_history_id'); $relations->belongsTo('host', Host::class)->setJoinType('LEFT'); $relations->belongsTo('service', Service::class)->setJoinType('LEFT'); } } icingadb-web-1.4.0/library/Icingadb/Model/Endpoint.php000066400000000000000000000037511516124654200225400ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $zone_id */ class Endpoint extends Model { public function getTableName() { return 'endpoint'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'zone_id' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'name_checksum' => t('Endpoint Name Checksum'), 'properties_checksum' => t('Endpoint Properties Checksum'), 'name' => t('Endpoint Name'), 'name_ci' => t('Endpoint Name (CI)'), 'zone_id' => t('Zone Id') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'environment_id', 'name_checksum', 'properties_checksum', 'zone_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->hasMany('host', Host::class) ->setForeignKey('command_endpoint_id'); $relations->hasMany('service', Service::class) ->setForeignKey('command_endpoint_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/Environment.php000066400000000000000000000117321516124654200232620ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $name */ class Environment extends Model { public function getTableName() { return 'environment'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'name' ]; } public function getColumnDefinitions() { return [ 'name' => t('Environment Name') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id' ])); } public function createRelations(Relations $relations) { $relations->hasMany('acknowledgement_history', AcknowledgementHistory::class); $relations->hasMany('action_url', ActionUrl::class); $relations->hasMany('checkcommand', Checkcommand::class); $relations->hasMany('checkcommand_argument', CheckcommandArgument::class); $relations->hasMany('checkcommand_customvar', CheckcommandCustomvar::class); $relations->hasMany('checkcommand_envvar', CheckcommandEnvvar::class); $relations->hasMany('comment', Comment::class); $relations->hasMany('comment_history', CommentHistory::class); $relations->hasMany('customvar', Customvar::class); $relations->hasMany('customvar_flat', CustomvarFlat::class); $relations->hasMany('downtime', Downtime::class); $relations->hasMany('downtime_history', DowntimeHistory::class); $relations->hasMany('endpoint', Endpoint::class); $relations->hasMany('eventcommand', Eventcommand::class); $relations->hasMany('eventcommand_argument', EventcommandArgument::class); $relations->hasMany('eventcommand_customvar', EventcommandCustomvar::class); $relations->hasMany('eventcommand_envvar', EventcommandEnvvar::class); $relations->hasMany('flapping_history', FlappingHistory::class); $relations->hasMany('history', History::class); $relations->hasMany('host', Host::class); $relations->hasMany('host_customvar', HostCustomvar::class); $relations->hasMany('host_state', HostState::class); $relations->hasMany('hostgroup', Hostgroup::class); $relations->hasMany('hostgroup_customvar', HostgroupCustomvar::class); $relations->hasMany('hostgroup_member', HostgroupMember::class); $relations->hasMany('instance', Instance::class); $relations->hasMany('icon_image', IconImage::class); $relations->hasMany('notes_url', NotesUrl::class); $relations->hasMany('notification', Notification::class); $relations->hasMany('notification_customvar', NotificationCustomvar::class); $relations->hasMany('notification_history', NotificationHistory::class); //$relations->hasMany('notification_recipient', NotificationRecipient::class); $relations->hasMany('notification_user', NotificationUser::class); $relations->hasMany('notification_usergroup', NotificationUsergroup::class); $relations->hasMany('notificationcommand', Notificationcommand::class); $relations->hasMany('notificationcommand_argument', NotificationcommandArgument::class); $relations->hasMany('notificationcommand_customvar', NotificationcommandCustomvar::class); $relations->hasMany('notificationcommand_envvar', NotificationcommandEnvvar::class); $relations->hasMany('service', Service::class); $relations->hasMany('service_customvar', ServiceCustomvar::class); $relations->hasMany('service_state', ServiceState::class); $relations->hasMany('servicegroup', Servicegroup::class); $relations->hasMany('servicegroup_customvar', ServicegroupCustomvar::class); $relations->hasMany('servicegroup_member', ServicegroupMember::class); $relations->hasMany('state_history', StateHistory::class); $relations->hasMany('timeperiod', Timeperiod::class); $relations->hasMany('timeperiod_customvar', TimeperiodCustomvar::class); $relations->hasMany('timeperiod_override_exclude', TimeperiodOverrideExclude::class); $relations->hasMany('timeperiod_override_include', TimeperiodOverrideInclude::class); $relations->hasMany('timeperiod_range', TimeperiodRange::class); $relations->hasMany('user', User::class); $relations->hasMany('user_customvar', UserCustomvar::class); //$relations->hasMany('user_notification_history', UserNotificationHistory::class); $relations->hasMany('usergroup', Usergroup::class); $relations->hasMany('usergroup_customvar', UsergroupCustomvar::class); $relations->hasMany('usergroup_member', UsergroupMember::class); $relations->hasMany('zone', Zone::class); } } icingadb-web-1.4.0/library/Icingadb/Model/Eventcommand.php000066400000000000000000000055571516124654200234060ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property ?string $zone_id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $command * @property int $timeout */ class Eventcommand extends Model { public function getTableName() { return 'eventcommand'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'zone_id', 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'command', 'timeout' ]; } public function getColumnDefinitions() { return [ 'zone_id' => t('Zone Id'), 'environment_id' => t('Environment Id'), 'name_checksum' => t('Eventcommand Name Checksum'), 'properties_checksum' => t('Eventcommand Properties Checksum'), 'name' => t('Eventcommand Name'), 'name_ci' => t('Eventcommand Name (CI)'), 'command' => t('Eventcommand'), 'timeout' => t('Eventcommand Timeout') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new ReRoute([ 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); $behaviors->add(new Binary([ 'id', 'zone_id', 'environment_id', 'name_checksum', 'properties_checksum' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->belongsToMany('customvar', Customvar::class) ->through(EventcommandCustomvar::class) ->setThroughAlias('t_eventcommand_customvar'); $relations->belongsToMany('customvar_flat', CustomvarFlat::class) ->through(EventcommandCustomvar::class); $relations->belongsToMany('vars', Vars::class) ->through(EventcommandCustomvar::class); $relations->hasMany('argument', EventcommandArgument::class); $relations->hasMany('envvar', EventcommandEnvvar::class); $relations->hasMany('host', Host::class); $relations->hasMany('service', Service::class); } } icingadb-web-1.4.0/library/Icingadb/Model/EventcommandArgument.php000066400000000000000000000054011516124654200250750ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $eventcommand_id * @property string $argument_key * @property string $environment_id * @property string $properties_checksum * @property ?string $argument_value * @property ?int $argument_order * @property ?string $description * @property ?string $argument_key_override * @property string $repeat_key * @property string $required * @property ?string $set_if * @property ?string $separator * @property string $skip_key */ class EventcommandArgument extends Model { public function getTableName() { return 'eventcommand_argument'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'eventcommand_id', 'argument_key', 'environment_id', 'properties_checksum', 'argument_value', 'argument_order', 'description', 'argument_key_override', 'repeat_key', 'required', 'set_if', 'separator', 'skip_key' ]; } public function getColumnDefinitions() { return [ 'eventcommand_id' => t('Eventcommand Id'), 'argument_key' => t('Eventcommand Argument Name'), 'environment_id' => t('Environment Id'), 'properties_checksum' => t('Eventcommand Argument Properties Checksum'), 'argument_value' => t('Eventcommand Argument Value'), 'argument_order' => t('Eventcommand Argument Position'), 'description' => t('Eventcommand Argument Description'), 'argument_key_override' => t('Eventcommand Argument Actual Name'), 'repeat_key' => t('Eventcommand Argument Repeated'), 'required' => t('Eventcommand Argument Required'), 'set_if' => t('Eventcommand Argument Condition'), 'skip_key' => t('Eventcommand Argument Without Name') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'eventcommand_id', 'environment_id', 'properties_checksum' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('eventcommand', Eventcommand::class); } } icingadb-web-1.4.0/library/Icingadb/Model/EventcommandCustomvar.php000066400000000000000000000026311516124654200253000ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $eventcommand_id * @property string $customvar_id * @property string $environment_id */ class EventcommandCustomvar extends Model { public function getTableName() { return 'eventcommand_customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'eventcommand_id', 'customvar_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'eventcommand_id', 'customvar_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('eventcommand', Eventcommand::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsTo('customvar_flat', CustomvarFlat::class) ->setCandidateKey('customvar_id') ->setForeignKey('customvar_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/EventcommandEnvvar.php000066400000000000000000000033571516124654200245640ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $eventcommand_id * @property string $envvar_key * @property string $environment_id * @property string $properties_checksum * @property string $envvar_value */ class EventcommandEnvvar extends Model { public function getTableName() { return 'eventcommand_envvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'eventcommand_id', 'envvar_key', 'environment_id', 'properties_checksum', 'envvar_value' ]; } public function getColumnDefinitions() { return [ 'eventcommand_id' => t('Eventcommand Id'), 'envvar_key' => t('Eventcommand Envvar Name'), 'environment_id' => t('Environment Id'), 'properties_checksum' => t('Eventcommand Envvar Properties Checksum'), 'envvar_value' => t('Eventcommand Envvar Value') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'eventcommand_id', 'environment_id', 'properties_checksum' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('eventcommand', Eventcommand::class); } } icingadb-web-1.4.0/library/Icingadb/Model/FlappingHistory.php000066400000000000000000000064311516124654200241000ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * Model for table `flapping_history` * * Please note that using this model will fetch history entries for decommissioned services. To avoid this, * the query needs a `flapping_history.service_id IS NULL OR flapping_history_service.id IS NOT NULL` where. * * @property string $id * @property string $environment_id * @property ?string $endpoint_id * @property string $object_type * @property string $host_id * @property ?string $service_id * @property DateTime $start_time * @property ?DateTime $end_time * @property ?float $percent_state_change_start * @property ?float $percent_state_change_end * @property float $flapping_threshold_low * @property float $flapping_threshold_high */ class FlappingHistory extends Model { public function getTableName() { return 'flapping_history'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'endpoint_id', 'object_type', 'host_id', 'service_id', 'start_time', 'end_time', 'percent_state_change_start', 'percent_state_change_end', 'flapping_threshold_low', 'flapping_threshold_high' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'endpoint_id' => t('Endpoint Id'), 'object_type' => t('Object Type'), 'host_id' => t('Host Id'), 'service_id' => t('Service Id'), 'start_time' => t('Flapping Start Time'), 'end_time' => t('Flapping End Time'), 'percent_state_change_start' => t('Flapping Percent State Change Start'), 'percent_state_change_end' => t('Flapping Percent State Change End'), 'flapping_threshold_low' => t('Flapping Threshold Low'), 'flapping_threshold_high' => t('Flapping Threshold High') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new MillisecondTimestamp([ 'start_time', 'end_time' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'endpoint_id', 'host_id', 'service_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('endpoint', Endpoint::class); $relations->belongsTo('environment', Environment::class); $relations->belongsTo('history', History::class) ->setCandidateKey('id') ->setForeignKey('flapping_history_id'); $relations->belongsTo('host', Host::class); $relations->belongsTo('service', Service::class)->setJoinType('LEFT'); } } icingadb-web-1.4.0/library/Icingadb/Model/History.php000066400000000000000000000106711516124654200224200ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * Model for table `history` * * Please note that using this model will fetch history entries for decommissioned services. To avoid * this, the query needs a `history.service_id IS NULL OR history_service.id IS NOT NULL` where. * * @property string $id * @property string $environment_id * @property ?string $endpoint_id * @property string $object_type * @property string $host_id * @property ?string $service_id * @property ?string $comment_history_id * @property ?string $downtime_history_id * @property ?string $flapping_history_id * @property ?string $notification_history_id * @property ?string $acknowledgement_history_id * @property ?string $state_history_id * @property string $event_type * @property DateTime $event_time */ class History extends Model { public function getTableName() { return 'history'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'endpoint_id', 'object_type', 'host_id', 'service_id', 'comment_history_id', 'downtime_history_id', 'flapping_history_id', 'notification_history_id', 'acknowledgement_history_id', 'state_history_id', 'event_type', 'event_time' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'endpoint_id' => t('Endpoint Id'), 'object_type' => t('Object Type'), 'host_id' => t('Host Id'), 'service_id' => t('Service Id'), 'event_type' => t('Event Type'), 'event_time' => t('Event Time') ]; } public function getDefaultSort() { return 'history.event_time desc, history.event_type desc'; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new MillisecondTimestamp([ 'event_time' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'endpoint_id', 'host_id', 'service_id', 'comment_history_id', 'downtime_history_id', 'flapping_history_id', 'notification_history_id', 'acknowledgement_history_id', 'state_history_id' ])); $behaviors->add(new ReRoute([ 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('endpoint', Endpoint::class); $relations->belongsTo('environment', Environment::class); $relations->belongsTo('host', Host::class); $relations->belongsTo('service', Service::class)->setJoinType('LEFT'); $relations->hasOne('comment', CommentHistory::class) ->setCandidateKey('comment_history_id') ->setForeignKey('comment_id') ->setJoinType('LEFT'); $relations->hasOne('downtime', DowntimeHistory::class) ->setCandidateKey('downtime_history_id') ->setForeignKey('downtime_id') ->setJoinType('LEFT'); $relations->hasOne('flapping', FlappingHistory::class) ->setCandidateKey('flapping_history_id') ->setForeignKey('id') ->setJoinType('LEFT'); $relations->hasOne('notification', NotificationHistory::class) ->setCandidateKey('notification_history_id') ->setForeignKey('id') ->setJoinType('LEFT'); $relations->hasOne('acknowledgement', AcknowledgementHistory::class) ->setCandidateKey('acknowledgement_history_id') ->setForeignKey('id') ->setJoinType('LEFT'); $relations->hasOne('state', StateHistory::class) ->setCandidateKey('state_history_id') ->setForeignKey('id') ->setJoinType('LEFT'); } } icingadb-web-1.4.0/library/Icingadb/Model/Host.php000066400000000000000000000263061516124654200216760ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Behavior\BoolCast; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use ipl\Orm\Defaults; use ipl\Orm\Relations; use ipl\Orm\ResultSet; /** * Host model. * * @property string $id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $display_name * @property string $address * @property string $address6 * @property ?string $address_bin * @property ?string $address6_bin * @property string $checkcommand_name * @property string $checkcommand_id * @property int $max_check_attempts * @property string $check_timeperiod_name * @property ?string $check_timeperiod_id * @property ?int $check_timeout * @property int $check_interval * @property int $check_retry_interval * @property bool $active_checks_enabled * @property bool $passive_checks_enabled * @property bool $event_handler_enabled * @property bool $notifications_enabled * @property bool $flapping_enabled * @property float $flapping_threshold_low * @property float $flapping_threshold_high * @property bool $perfdata_enabled * @property string $eventcommand_name * @property ?string $eventcommand_id * @property bool $is_volatile * @property ?string $action_url_id * @property ?string $notes_url_id * @property string $notes * @property ?string $icon_image_id * @property string $icon_image_alt * @property string $zone_name * @property ?string $zone_id * @property string $command_endpoint_name * @property ?string $command_endpoint_id * @property ?int $total_children */ class Host extends Model { use Auth; public function getTableName() { return 'host'; } public function getKeyName() { return 'id'; } public function getColumns() { $columns = [ 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'display_name', 'address', 'address6', 'address_bin', 'address6_bin', 'checkcommand_name', 'checkcommand_id', 'max_check_attempts', 'check_timeperiod_name', 'check_timeperiod_id', 'check_timeout', 'check_interval', 'check_retry_interval', 'active_checks_enabled', 'passive_checks_enabled', 'event_handler_enabled', 'notifications_enabled', 'flapping_enabled', 'flapping_threshold_low', 'flapping_threshold_high', 'perfdata_enabled', 'eventcommand_name', 'eventcommand_id', 'is_volatile', 'action_url_id', 'notes_url_id', 'notes', 'icon_image_id', 'icon_image_alt', 'zone_name', 'zone_id', 'command_endpoint_name', 'command_endpoint_id' ]; if (Backend::supportsDependencies()) { $columns[] = 'total_children'; } return $columns; } public function getColumnDefinitions() { $columns = [ 'environment_id' => t('Environment Id'), 'name_checksum' => t('Host Name Checksum'), 'properties_checksum' => t('Host Properties Checksum'), 'name' => t('Host Name'), 'name_ci' => t('Host Name (CI)'), 'display_name' => t('Host Display Name'), 'address' => t('Host Address (IPv4)'), 'address6' => t('Host Address (IPv6)'), 'address_bin' => t('Host Address (IPv4, Binary)'), 'address6_bin' => t('Host Address (IPv6, Binary)'), 'checkcommand_name' => t('Checkcommand Name'), 'checkcommand_id' => t('Checkcommand Id'), 'max_check_attempts' => t('Host Max Check Attempts'), 'check_timeperiod_name' => t('Check Timeperiod Name'), 'check_timeperiod_id' => t('Check Timeperiod Id'), 'check_timeout' => t('Host Check Timeout'), 'check_interval' => t('Host Check Interval'), 'check_retry_interval' => t('Host Check Retry Interval'), 'active_checks_enabled' => t('Host Active Checks Enabled'), 'passive_checks_enabled' => t('Host Passive Checks Enabled'), 'event_handler_enabled' => t('Host Event Handler Enabled'), 'notifications_enabled' => t('Host Notifications Enabled'), 'flapping_enabled' => t('Host Flapping Enabled'), 'flapping_threshold_low' => t('Host Flapping Threshold Low'), 'flapping_threshold_high' => t('Host Flapping Threshold High'), 'perfdata_enabled' => t('Host Performance Data Enabled'), 'eventcommand_name' => t('Eventcommand Name'), 'eventcommand_id' => t('Eventcommand Id'), 'is_volatile' => t('Host Is Volatile'), 'action_url_id' => t('Action Url Id'), 'notes_url_id' => t('Notes Url Id'), 'notes' => t('Host Notes'), 'icon_image_id' => t('Icon Image Id'), 'icon_image_alt' => t('Icon Image Alt'), 'zone_name' => t('Zone Name'), 'zone_id' => t('Zone Id'), 'command_endpoint_name' => t('Endpoint Name'), 'command_endpoint_id' => t('Endpoint Id') ]; if (Backend::supportsDependencies()) { $columns['total_children'] = t('Total Children'); } return $columns; } public function getSearchColumns() { return ['name_ci', 'display_name']; } public function getDefaultSort() { return 'host.display_name'; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new BoolCast([ 'active_checks_enabled', 'passive_checks_enabled', 'event_handler_enabled', 'notifications_enabled', 'flapping_enabled', 'is_volatile' ])); $behaviors->add(new ReRoute([ 'child' => 'to.from', 'parent' => 'from.to', 'servicegroup' => 'service.servicegroup', 'user' => 'notification.user', 'usergroup' => 'notification.usergroup' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'name_checksum', 'properties_checksum', 'address_bin', 'address6_bin', 'checkcommand_id', 'check_timeperiod_id', 'eventcommand_id', 'action_url_id', 'notes_url_id', 'icon_image_id', 'zone_id', 'command_endpoint_id' ])); } public function createDefaults(Defaults $defaults) { $defaults->add('vars', function (self $subject) { if (! $subject->customvar_flat instanceof ResultSet) { $this->applyRestrictions($subject->customvar_flat); } $vars = []; foreach ($subject->customvar_flat as $customVar) { $vars[$customVar->flatname] = $customVar->flatvalue; } return $vars; }); $defaults->add('customvars', function (self $subject) { // In contrast to customvar_flat, we cannot apply restrictions here as denylists and protections // require the flattened values to work. To retrieve only customvars the user has access to, the // `vars` property should be used instead. $vars = []; foreach ($subject->customvar as $customVar) { $vars[$customVar->name] = json_decode($customVar->value, true); } return $vars; }); } public function createRelations(Relations $relations) { $relations->hasOne('state', HostState::class)->setJoinType('LEFT'); $relations->hasOne('dependency_node', DependencyNode::class)->setJoinType('LEFT'); $relations->hasOne('unreachable_parent', UnreachableParent::class)->setJoinType('LEFT'); $relations->belongsTo('environment', Environment::class); $relations->belongsTo('eventcommand', Eventcommand::class); $relations->belongsTo('checkcommand', Checkcommand::class); $relations->belongsTo('timeperiod', Timeperiod::class) ->setCandidateKey('check_timeperiod_id') ->setJoinType('LEFT'); $relations->belongsTo('action_url', ActionUrl::class) ->setCandidateKey('action_url_id') ->setForeignKey('id'); $relations->belongsTo('notes_url', NotesUrl::class) ->setCandidateKey('notes_url_id') ->setForeignKey('id'); $relations->belongsTo('icon_image', IconImage::class) ->setCandidateKey('icon_image_id') ->setJoinType('LEFT'); $relations->belongsTo('zone', Zone::class); $relations->belongsTo('endpoint', Endpoint::class) ->setCandidateKey('command_endpoint_id'); $relations->belongsToMany('customvar', Customvar::class) ->through(HostCustomvar::class) ->setThroughAlias('t_host_customvar'); $relations->belongsToMany('customvar_flat', CustomvarFlat::class) ->through(HostCustomvar::class); $relations->belongsToMany('vars', Vars::class) ->through(HostCustomvar::class); $relations->belongsToMany('hostgroup', Hostgroup::class) ->through(HostgroupMember::class); $relations->hasMany('comment', Comment::class)->setJoinType('LEFT'); $relations->hasMany('downtime', Downtime::class)->setJoinType('LEFT'); $relations->hasMany('sla_history_downtime', SlaHistoryDowntime::class)->setJoinType('LEFT'); $relations->hasMany('history', History::class); $relations->hasMany('sla_history_state', SlaHistoryState::class); $relations->hasMany('notification', Notification::class)->setJoinType('LEFT'); $relations->hasMany('notification_history', NotificationHistory::class); $relations->hasMany('service', Service::class)->setJoinType('LEFT'); $relations->belongsToMany('from', DependencyEdge::class) ->setTargetCandidateKey('from_node_id') ->setTargetForeignKey('id') ->through(DependencyNode::class); $relations->belongsToMany('to', DependencyEdge::class) ->setTargetCandidateKey('to_node_id') ->setTargetForeignKey('id') ->through(DependencyNode::class); } } icingadb-web-1.4.0/library/Icingadb/Model/HostCustomvar.php000066400000000000000000000025411516124654200235750ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $host_id * @property string $customvar_id * @property string $environment_id */ class HostCustomvar extends Model { public function getTableName() { return 'host_customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'host_id', 'customvar_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'host_id', 'customvar_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('host', Host::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsTo('customvar_flat', CustomvarFlat::class) ->setForeignKey('customvar_id') ->setCandidateKey('customvar_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/HostState.php000066400000000000000000000070231516124654200226720ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Common\HostStates; use ipl\Orm\Relations; /** * Host state model. */ class HostState extends State { public function getTableName() { return 'host_state'; } public function getKeyName() { return 'host_id'; } public function getColumnDefinitions() { $columns = [ 'environment_id' => t('Environment Id'), 'state_type' => t('Host State Type'), 'soft_state' => t('Host Soft State'), 'hard_state' => t('Host Hard State'), 'previous_soft_state' => t('Host Previous Soft State'), 'previous_hard_state' => t('Host Previous Hard State'), 'check_attempt' => t('Host Check Attempt No.'), 'severity' => t('Host State Severity'), 'output' => t('Host Output'), 'long_output' => t('Host Long Output'), 'performance_data' => t('Host Performance Data'), 'normalized_performance_data' => t('Host Normalized Performance Data'), 'check_commandline' => t('Host Check Commandline'), 'is_problem' => t('Host Has Problem'), 'is_handled' => t('Host Is Handled'), 'is_reachable' => t('Host Is Reachable'), 'is_flapping' => t('Host Is Flapping'), 'is_overdue' => t('Host Check Is Overdue'), 'is_acknowledged' => t('Host Is Acknowledged'), 'acknowledgement_comment_id' => t('Acknowledgement Comment Id'), 'in_downtime' => t('Host In Downtime'), 'execution_time' => t('Host Check Execution Time'), 'latency' => t('Host Check Latency'), 'check_timeout' => t('Host Check Timeout'), 'check_source' => t('Host Check Source'), 'scheduling_source' => t('Host Scheduling Source'), 'last_update' => t('Host Last Update'), 'last_state_change' => t('Host Last State Change'), 'next_check' => t('Host Next Check'), 'next_update' => t('Host Next Update') ]; if (Backend::supportsDependencies()) { $columns['affects_children'] = t('Host Affects Children'); $columns['is_sticky_acknowledgement'] = t('Acknowledgement Is Sticky'); } return $columns; } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('host', Host::class); $relations->hasOne('last_comment', LastHostComment::class) ->setCandidateKey('last_comment_id') ->setForeignKey('id') ->setJoinType('LEFT'); } public function getStateText(): string { return HostStates::text($this->soft_state); } public function getStateTextTranslated(): string { return HostStates::translated($this->soft_state); } } icingadb-web-1.4.0/library/Icingadb/Model/Hostgroup.php000066400000000000000000000056571516124654200227610ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $display_name * @property ?string $zone_id */ class Hostgroup extends Model { public function getTableName() { return 'hostgroup'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'display_name', 'zone_id' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'name_checksum' => t('Hostgroup Name Checksum'), 'properties_checksum' => t('Hostgroup Properties Checksum'), 'name' => t('Hostgroup Name'), 'name_ci' => t('Hostgroup Name (CI)'), 'display_name' => t('Hostgroup Display Name'), 'zone_id' => t('Zone Id') ]; } public function getSearchColumns() { return ['name_ci', 'display_name']; } public function getDefaultSort() { return 'display_name'; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new ReRoute([ 'servicegroup' => 'service.servicegroup', 'parent' => 'host.from.to', 'child' => 'host.to.from' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'name_checksum', 'properties_checksum', 'zone_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->belongsToMany('customvar', Customvar::class) ->through(HostgroupCustomvar::class) ->setThroughAlias('t_hostgroup_customvar'); $relations->belongsToMany('customvar_flat', CustomvarFlat::class) ->through(HostgroupCustomvar::class); $relations->belongsToMany('vars', Vars::class) ->through(HostgroupCustomvar::class); $relations->belongsToMany('host', Host::class) ->through(HostgroupMember::class); $relations->belongsToMany('service', Service::class) ->through(HostgroupMember::class); } } icingadb-web-1.4.0/library/Icingadb/Model/HostgroupCustomvar.php000066400000000000000000000026041516124654200246520ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $hostgroup_id * @property string $customvar_id * @property string $environment_id */ class HostgroupCustomvar extends Model { public function getTableName() { return 'hostgroup_customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'hostgroup_id', 'customvar_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'hostgroup_id', 'customvar_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('hostgroup', Hostgroup::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsTo('customvar_flat', CustomvarFlat::class) ->setCandidateKey('customvar_id') ->setForeignKey('customvar_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/HostgroupMember.php000066400000000000000000000025151516124654200240770ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $host_id * @property string $hostgroup_id * @property string $environment_id */ class HostgroupMember extends Model { public function getTableName() { return 'hostgroup_member'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'host_id', 'hostgroup_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'host_id', 'hostgroup_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('hostgroup', Hostgroup::class); $relations->belongsTo('host', Host::class); $relations->hasMany('service', Service::class) ->setForeignKey('host_id') ->setCandidateKey('host_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/Hostgroupsummary.php000066400000000000000000000230461516124654200243670ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Common\Auth; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use ipl\Orm\Query; use ipl\Orm\Relations; use ipl\Orm\UnionModel; use ipl\Orm\UnionQuery; use ipl\Sql\Adapter\Pgsql; use ipl\Sql\Connection; use ipl\Sql\Expression; use ipl\Sql\Select; /** * @property string $id * @property string $display_name * @property string $name_ci * @property string $name * @property int $hosts_down_handled * @property int $hosts_down_unhandled * @property int $hosts_pending * @property int $hosts_total * @property int $hosts_up * @property int $hosts_severity * @property int $services_critical_handled * @property int $services_critical_unhandled * @property int $services_ok * @property int $services_pending * @property int $services_total * @property int $services_unknown_handled * @property int $services_unknown_unhandled * @property int $services_warning_handled * @property int $services_warning_unhandled */ class Hostgroupsummary extends UnionModel { public static function on(Connection $db): UnionQuery { $q = parent::on($db); $q->on( Query::ON_SELECT_ASSEMBLED, function () use ($q) { $auth = new class () { use Auth; }; $auth->assertColumnRestrictions($q->getFilter()); } ); $q->on($q::ON_SELECT_ASSEMBLED, function (Select $select) use ($q) { $model = $q->getModel(); $groupBy = $q->getResolver()->qualifyColumnsAndAliases((array) $model->getKeyName(), $model, false); // For PostgreSQL, ALL non-aggregate SELECT columns must appear in the GROUP BY clause: if ($q->getDb()->getAdapter() instanceof Pgsql) { /** * Ignore Expressions, i.e. aggregate functions {@see getColumns()}, * which do not need to be added to the GROUP BY. */ $candidates = array_filter($select->getColumns(), 'is_string'); // Remove already considered columns for the GROUP BY, i.e. the primary key. $candidates = array_diff_assoc($candidates, $groupBy); $groupBy = array_merge($groupBy, $candidates); } $select->groupBy($groupBy); }); return $q; } public function getTableName() { return 'hostgroup'; } public function getKeyName() { return ['id' => 'hostgroup_id']; } public function getColumns() { return [ 'name' => 'hostgroup_name', 'name_ci' => 'hostgroup_name_ci', 'display_name' => 'hostgroup_display_name', 'hosts_down_handled' => new Expression( 'SUM(CASE WHEN host_state = 1' . ' AND (host_handled = \'y\' OR host_reachable = \'n\') THEN 1 ELSE 0 END)' ), 'hosts_down_unhandled' => new Expression( 'SUM(CASE WHEN host_state = 1' . ' AND host_handled = \'n\' AND host_reachable = \'y\' THEN 1 ELSE 0 END)' ), 'hosts_pending' => new Expression( 'SUM(CASE WHEN host_state = 99 THEN 1 ELSE 0 END)' ), 'hosts_total' => new Expression( 'SUM(CASE WHEN host_id IS NOT NULL THEN 1 ELSE 0 END)' ), 'hosts_up' => new Expression( 'SUM(CASE WHEN host_state = 0 THEN 1 ELSE 0 END)' ), 'hosts_severity' => new Expression('MAX(host_severity)'), 'services_critical_handled' => new Expression( 'SUM(CASE WHEN service_state = 2' . ' AND (service_handled = \'y\' OR service_reachable = \'n\') THEN 1 ELSE 0 END)' ), 'services_critical_unhandled' => new Expression( 'SUM(CASE WHEN service_state = 2' . ' AND service_handled = \'n\' AND service_reachable = \'y\' THEN 1 ELSE 0 END)' ), 'services_ok' => new Expression( 'SUM(CASE WHEN service_state = 0 THEN 1 ELSE 0 END)' ), 'services_pending' => new Expression( 'SUM(CASE WHEN service_state = 99 THEN 1 ELSE 0 END)' ), 'services_total' => new Expression( 'SUM(CASE WHEN service_id IS NOT NULL THEN 1 ELSE 0 END)' ), 'services_unknown_handled' => new Expression( 'SUM(CASE WHEN service_state = 3' . ' AND (service_handled = \'y\' OR service_reachable = \'n\') THEN 1 ELSE 0 END)' ), 'services_unknown_unhandled' => new Expression( 'SUM(CASE WHEN service_state = 3' . ' AND service_handled = \'n\' AND service_reachable = \'y\' THEN 1 ELSE 0 END)' ), 'services_warning_handled' => new Expression( 'SUM(CASE WHEN service_state = 1' . ' AND (service_handled = \'y\' OR service_reachable = \'n\') THEN 1 ELSE 0 END)' ), 'services_warning_unhandled' => new Expression( 'SUM(CASE WHEN service_state = 1' . ' AND service_handled = \'n\' AND service_reachable = \'y\' THEN 1 ELSE 0 END)' ) ]; } public function getSearchColumns() { return ['name_ci', 'display_name']; } public function getDefaultSort() { return 'display_name'; } public function getUnions(): array { $unions = [ [ Host::class, [ 'hostgroup', 'state' ], [ 'hostgroup_id' => 'hostgroup.id', 'hostgroup_name' => 'hostgroup.name', 'hostgroup_name_ci' => 'hostgroup.name_ci', 'hostgroup_display_name' => 'hostgroup.display_name', 'host_id' => 'host.id', 'host_state' => 'state.soft_state', 'host_handled' => 'state.is_handled', 'host_reachable' => 'state.is_reachable', 'host_severity' => 'state.severity', 'service_id' => new Expression('NULL'), 'service_state' => new Expression('NULL'), 'service_handled' => new Expression('NULL'), 'service_reachable' => new Expression('NULL') ] ], [ Service::class, [ 'hostgroup', 'state' ], [ 'hostgroup_id' => 'hostgroup.id', 'hostgroup_name' => 'hostgroup.name', 'hostgroup_name_ci' => 'hostgroup.name_ci', 'hostgroup_display_name' => 'hostgroup.display_name', 'host_id' => new Expression('NULL'), 'host_state' => new Expression('NULL'), 'host_handled' => new Expression('NULL'), 'host_reachable' => new Expression('NULL'), 'host_severity' => new Expression('0'), 'service_id' => 'service.id', 'service_state' => 'state.soft_state', 'service_handled' => 'state.is_handled', 'service_reachable' => 'state.is_reachable' ] ], [ Hostgroup::class, [], [ 'hostgroup_id' => 'hostgroup.id', 'hostgroup_name' => 'hostgroup.name', 'hostgroup_name_ci' => 'hostgroup.name_ci', 'hostgroup_display_name' => 'hostgroup.display_name', 'host_id' => new Expression('NULL'), 'host_state' => new Expression('NULL'), 'host_handled' => new Expression('NULL'), 'host_reachable' => new Expression('NULL'), 'host_severity' => new Expression('0'), 'service_id' => new Expression('NULL'), 'service_state' => new Expression('NULL'), 'service_handled' => new Expression('NULL'), 'service_reachable' => new Expression('NULL') ] ] ]; return $unions; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id' ])); // This is because there is no better way (new Hostgroup())->createBehaviors($behaviors); } public function createRelations(Relations $relations) { // This is because there is no better way (new Hostgroup())->createRelations($relations); } public function getColumnDefinitions() { // This is because there is no better way return (new Hostgroup())->getColumnDefinitions(); } } icingadb-web-1.4.0/library/Icingadb/Model/HoststateSummary.php000066400000000000000000000063471516124654200243200ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Sql\Connection; use ipl\Sql\Expression; /** * @property int $hosts_acknowledged * @property int $hosts_active_checks_enabled * @property int $hosts_passive_checks_enabled * @property int $hosts_down_handled * @property int $hosts_down_unhandled * @property int $hosts_event_handler_enabled * @property int $hosts_flapping_enabled * @property int $hosts_notifications_enabled * @property int $hosts_pending * @property int $hosts_problems_unacknowledged * @property int $hosts_total * @property int $hosts_up */ class HoststateSummary extends Host { public function getSummaryColumns() { return [ 'hosts_acknowledged' => new Expression( 'SUM(CASE WHEN host_state.is_acknowledged = \'y\' THEN 1 ELSE 0 END)' ), 'hosts_active_checks_enabled' => new Expression( 'SUM(CASE WHEN host.active_checks_enabled = \'y\' THEN 1 ELSE 0 END)' ), 'hosts_passive_checks_enabled' => new Expression( 'SUM(CASE WHEN host.passive_checks_enabled = \'y\' THEN 1 ELSE 0 END)' ), 'hosts_down_handled' => new Expression( 'SUM(CASE WHEN host_state.soft_state = 1' . ' AND (host_state.is_handled = \'y\' OR host_state.is_reachable = \'n\') THEN 1 ELSE 0 END)' ), 'hosts_down_unhandled' => new Expression( 'SUM(CASE WHEN host_state.soft_state = 1' . ' AND host_state.is_handled = \'n\' AND host_state.is_reachable = \'y\' THEN 1 ELSE 0 END)' ), 'hosts_event_handler_enabled' => new Expression( 'SUM(CASE WHEN host.event_handler_enabled = \'y\' THEN 1 ELSE 0 END)' ), 'hosts_flapping_enabled' => new Expression( 'SUM(CASE WHEN host.flapping_enabled = \'y\' THEN 1 ELSE 0 END)' ), 'hosts_notifications_enabled' => new Expression( 'SUM(CASE WHEN host.notifications_enabled = \'y\' THEN 1 ELSE 0 END)' ), 'hosts_pending' => new Expression( 'SUM(CASE WHEN host_state.soft_state = 99 THEN 1 ELSE 0 END)' ), 'hosts_problems_unacknowledged' => new Expression( 'SUM(CASE WHEN host_state.is_problem = \'y\'' . ' AND host_state.is_acknowledged = \'n\' THEN 1 ELSE 0 END)' ), 'hosts_total' => new Expression( 'SUM(CASE WHEN host.id IS NOT NULL THEN 1 ELSE 0 END)' ), 'hosts_up' => new Expression( 'SUM(CASE WHEN host_state.soft_state = 0 THEN 1 ELSE 0 END)' ) ]; } public static function on(Connection $db) { $q = parent::on($db); $q->utilize('state'); /** @var static $m */ $m = $q->getModel(); $q->columns($m->getSummaryColumns()); return $q; } public function getColumns() { return array_merge(parent::getColumns(), $this->getSummaryColumns()); } public function getDefaultSort() { return null; } } icingadb-web-1.4.0/library/Icingadb/Model/IconImage.php000066400000000000000000000024051516124654200226060ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $icon_image * @property string $environment_id */ class IconImage extends Model { public function getTableName() { return 'icon_image'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'icon_image', 'environment_id' ]; } public function getColumnDefinitions() { return [ 'icon_image' => t('Icon Image'), 'environment_id' => t('Environment Id') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->hasMany('host', Host::class); $relations->hasMany('service', Service::class); } } icingadb-web-1.4.0/library/Icingadb/Model/Instance.php000066400000000000000000000054561516124654200225300ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use Icinga\Module\Icingadb\Common\Backend; use ipl\Orm\Behavior\BoolCast; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property ?string $endpoint_id * @property DateTime $heartbeat * @property bool $responsible * @property bool $icinga2_active_host_checks_enabled * @property bool $icinga2_active_service_checks_enabled * @property bool $icinga2_event_handlers_enabled * @property bool $icinga2_flap_detection_enabled * @property bool $icinga2_notifications_enabled * @property bool $icinga2_performance_data_enabled * @property DateTime $icinga2_start_time * @property string $icinga2_version * @property string $icingadb_version */ class Instance extends Model { public function getTableName() { return 'icingadb_instance'; } public function getKeyName() { return 'id'; } public function getColumns() { $columns = [ 'environment_id', 'endpoint_id', 'heartbeat', 'responsible', 'icinga2_active_host_checks_enabled', 'icinga2_active_service_checks_enabled', 'icinga2_event_handlers_enabled', 'icinga2_flap_detection_enabled', 'icinga2_notifications_enabled', 'icinga2_performance_data_enabled', 'icinga2_start_time', 'icinga2_version' ]; if (Backend::supportsDependencies()) { $columns[] = 'icingadb_version'; } return $columns; } public function getDefaultSort() { return ['responsible desc', 'heartbeat desc']; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new MillisecondTimestamp([ 'heartbeat', 'icinga2_start_time' ])); $behaviors->add(new BoolCast([ 'responsible', 'icinga2_active_host_checks_enabled', 'icinga2_active_service_checks_enabled', 'icinga2_event_handlers_enabled', 'icinga2_flap_detection_enabled', 'icinga2_notifications_enabled', 'icinga2_performance_data_enabled' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'endpoint_id', ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('endpoint', Endpoint::class)->setJoinType('LEFT'); } } icingadb-web-1.4.0/library/Icingadb/Model/LastHostComment.php000066400000000000000000000010601516124654200240330ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Relations; class LastHostComment extends Comment { public function createRelations(Relations $relations): void { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->belongsTo('state', HostState::class) ->setForeignKey('last_comment_id') ->setCandidateKey('id'); } } icingadb-web-1.4.0/library/Icingadb/Model/LastServiceComment.php000066400000000000000000000010661516124654200245240ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Relations; class LastServiceComment extends Comment { public function createRelations(Relations $relations): void { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->belongsTo('state', ServiceState::class) ->setForeignKey('last_comment_id') ->setCandidateKey('id'); } } icingadb-web-1.4.0/library/Icingadb/Model/NotesUrl.php000066400000000000000000000030351516124654200225260ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\ActionAndNoteUrl; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string[] $notes_url * @property string $environment_id */ class NotesUrl extends Model { public function getTableName() { return 'notes_url'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'notes_url', 'environment_id' ]; } public function getColumnDefinitions() { return [ 'notes_url' => t('Notes Url'), 'environment_id' => t('Environment Id') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new ActionAndNoteUrl(['notes_url'])); $behaviors->add(new Binary([ 'id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->hasMany('host', Host::class) ->setCandidateKey('id') ->setForeignKey('notes_url_id'); $relations->hasMany('service', Service::class) ->setCandidateKey('id') ->setForeignKey('notes_url_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/Notification.php000066400000000000000000000115571516124654200234110ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\Bitmask; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $host_id * @property ?string $service_id * @property string $notificationcommand_id * @property ?int $times_begin * @property ?int $times_end * @property int $notification_interval * @property ?string $timeperiod_id * @property string[] $states * @property string[] $types * @property ?string $zone_id */ class Notification extends Model { public function getTableName() { return 'notification'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'host_id', 'service_id', 'notificationcommand_id', 'times_begin', 'times_end', 'notification_interval', 'timeperiod_id', 'states', 'types', 'zone_id' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'name_checksum' => t('Notification Name Checksum'), 'properties_checksum' => t('Notification Properties Checksum'), 'name' => t('Notification Name'), 'name_ci' => t('Notification Name (CI)'), 'host_id' => t('Host Id'), 'service_id' => t('Service Id'), 'notificationcommand_id' => t('Notificationcommand Id'), 'times_begin' => t('Notification Escalate After'), 'times_end' => t('Notification Escalate Until'), 'notification_interval' => t('Notification Interval'), 'timeperiod_id' => t('Timeperiod Id'), 'states' => t('Notification State Filter'), 'types' => t('Notification Type Filter'), 'zone_id' => t('Zone Id') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new ReRoute([ 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); $behaviors->add(new Bitmask([ 'states' => [ 'ok' => 1, 'warning' => 2, 'critical' => 4, 'unknown' => 8, 'up' => 16, 'down' => 32 ], 'types' => [ 'downtime_start' => 1, 'downtime_end' => 2, 'downtime_removed' => 4, 'custom' => 8, 'ack' => 16, 'problem' => 32, 'recovery' => 64, 'flapping_start' => 128, 'flapping_end' => 256 ] ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'name_checksum', 'properties_checksum', 'host_id', 'service_id', 'notificationcommand_id', 'timeperiod_id', 'zone_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('host', Host::class); $relations->belongsTo('service', Service::class); $relations->belongsTo('notificationcommand', Notificationcommand::class); $relations->belongsTo('timeperiod', Timeperiod::class); $relations->belongsTo('zone', Zone::class); $relations->belongsToMany('customvar', Customvar::class) ->through(NotificationCustomvar::class) ->setThroughAlias('t_notification_customvar'); $relations->belongsToMany('customvar_flat', CustomvarFlat::class) ->through(NotificationCustomvar::class); $relations->belongsToMany('vars', Vars::class) ->through(NotificationCustomvar::class); $relations->belongsToMany('user', User::class) ->through('notification_recipient'); $relations->belongsToMany('usergroup', Usergroup::class) ->through('notification_recipient'); } } icingadb-web-1.4.0/library/Icingadb/Model/NotificationCustomvar.php000066400000000000000000000026311516124654200253060ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $notification_id * @property string $customvar_id * @property string $environment_id */ class NotificationCustomvar extends Model { public function getTableName() { return 'notification_customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'notification_id', 'customvar_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'notification_id', 'customvar_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('notification', Notification::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsTo('customvar_flat', CustomvarFlat::class) ->setCandidateKey('customvar_id') ->setForeignKey('customvar_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/NotificationHistory.php000066400000000000000000000075501516124654200247710ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * Model for table `notification_history` * * Please note that using this model will fetch history entries for decommissioned services. To avoid this, the * query needs a `notification_history.service_id IS NULL OR notification_history_service.id IS NOT NULL` where. * * @property string $id * @property string $environment_id * @property ?string $endpoint_id * @property string $object_type * @property string $host_id * @property ?string $service_id * @property string $notification_id * @property string $type * @property DateTime $send_time * @property int $state * @property int $previous_hard_state * @property string $author * @property string $text * @property int $users_notified */ class NotificationHistory extends Model { public function getTableName() { return 'notification_history'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'endpoint_id', 'object_type', 'host_id', 'service_id', 'notification_id', 'type', 'send_time', 'state', 'previous_hard_state', 'author', 'text', 'users_notified' ]; } public function getColumnDefinitions() { return [ 'id' => t('History Id'), 'environment_id' => t('Environment Id'), 'endpoint_id' => t('Endpoint Id'), 'object_type' => t('Object Type'), 'host_id' => t('Host Id'), 'service_id' => t('Service Id'), 'notification_id' => t('Notification Id'), 'type' => t('Notification Type'), 'send_time' => t('Notification Sent On'), 'state' => t('Hard State'), 'previous_hard_state' => t('Previous Hard State'), 'author' => t('Notification Author'), 'text' => t('Notification Text'), 'users_notified' => t('Contacts Notified') ]; } public function getSearchColumns() { return ['text']; } public function getDefaultSort() { return 'notification_history.send_time desc'; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new MillisecondTimestamp([ 'send_time' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'endpoint_id', 'host_id', 'service_id', 'notification_id' ])); $behaviors->add(new ReRoute([ 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('history', History::class) ->setCandidateKey('id') ->setForeignKey('notification_history_id'); $relations->belongsTo('host', Host::class); $relations->belongsTo('service', Service::class)->setJoinType('LEFT'); $relations->belongsTo('notification', Notification::class)->setJoinType('LEFT'); $relations->belongsToMany('user', User::class) ->through('user_notification_history'); } } icingadb-web-1.4.0/library/Icingadb/Model/NotificationUser.php000066400000000000000000000023251516124654200242410ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $notification_id * @property string $user_id * @property string $environment_id */ class NotificationUser extends Model { public function getTableName() { return 'notification_user'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'notification_id', 'user_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'notification_id', 'user_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('notification', Notification::class); $relations->belongsTo('user', User::class); } } icingadb-web-1.4.0/library/Icingadb/Model/NotificationUsergroup.php000066400000000000000000000023701516124654200253160ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $notification_id * @property string $usergroup_id * @property string $environment_id */ class NotificationUsergroup extends Model { public function getTableName() { return 'notification_usergroup'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'notification_id', 'usergroup_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'notification_id', 'usergroup_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('notification', Notification::class); $relations->belongsTo('usergroup', Usergroup::class); } } icingadb-web-1.4.0/library/Icingadb/Model/Notificationcommand.php000066400000000000000000000060561516124654200247460ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property ?string $zone_id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $command * @property int $timeout */ class Notificationcommand extends Model { public function getTableName() { return 'notificationcommand'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'zone_id', 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'command', 'timeout' ]; } public function getColumnDefinitions() { return [ 'zone_id' => t('Zone Id'), 'environment_id' => t('Environment Id'), 'name_checksum' => t('Notificationcommand Name Checksum'), 'properties_checksum' => t('Notificationcommand Properties Checksum'), 'name' => t('Notificationcommand Name'), 'name_ci' => t('Notificationcommand Name (CI)'), 'command' => t('Notificationcommand'), 'timeout' => t('Notificationcommand Timeout') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new ReRoute([ 'host' => 'notification.host', 'hostgroup' => 'notification.host.hostgroup', 'service' => 'notification.service', 'servicegroup' => 'notification.service.servicegroup' ])); $behaviors->add(new Binary([ 'id', 'zone_id', 'environment_id', 'name_checksum', 'properties_checksum' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->belongsToMany('customvar', Customvar::class) ->through(NotificationcommandCustomvar::class) ->setThroughAlias('t_notificationcommand_customvar'); $relations->belongsToMany('customvar_flat', CustomvarFlat::class) ->through(NotificationcommandCustomvar::class); $relations->belongsToMany('vars', Vars::class) ->through(NotificationcommandCustomvar::class); $relations->hasMany('notification', Notification::class); $relations->hasMany('argument', NotificationcommandArgument::class); $relations->hasMany('envvar', NotificationcommandEnvvar::class); } } icingadb-web-1.4.0/library/Icingadb/Model/NotificationcommandArgument.php000066400000000000000000000056131516124654200264470ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $notificationcommand_id * @property string $argument_key * @property string $environment_id * @property string $properties_checksum * @property ?string $argument_value * @property ?int $argument_order * @property ?string $description * @property ?string $argument_key_override * @property string $repeat_key * @property string $required * @property ?string $set_if * @property ?string $separator * @property string $skip_key */ class NotificationcommandArgument extends Model { public function getTableName() { return 'notificationcommand_argument'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'notificationcommand_id', 'argument_key', 'environment_id', 'properties_checksum', 'argument_value', 'argument_order', 'description', 'argument_key_override', 'repeat_key', 'required', 'set_if', 'separator', 'skip_key' ]; } public function getColumnDefinitions() { return [ 'notificationcommand_id' => t('Notificationcommand Id'), 'argument_key' => t('Notificationcommand Argument Name'), 'environment_id' => t('Environment Id'), 'properties_checksum' => t('Notificationcommand Argument Properties Checksum'), 'argument_value' => t('Notificationcommand Argument Value'), 'argument_order' => t('Notificationcommand Argument Position'), 'description' => t('Notificationcommand Argument Description'), 'argument_key_override' => t('Notificationcommand Argument Actual Name'), 'repeat_key' => t('Notificationcommand Argument Repeated'), 'required' => t('Notificationcommand Argument Required'), 'set_if' => t('Notificationcommand Argument Condition'), 'skip_key' => t('Notificationcommand Argument Without Name') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'notificationcommand_id', 'environment_id', 'properties_checksum' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('notificationcommand', Notificationcommand::class); } } icingadb-web-1.4.0/library/Icingadb/Model/NotificationcommandCustomvar.php000066400000000000000000000027121516124654200266450ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $notificationcommand_id * @property string $customvar_id * @property string $environment_id */ class NotificationcommandCustomvar extends Model { public function getTableName() { return 'notificationcommand_customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'notificationcommand_id', 'customvar_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'notificationcommand_id', 'customvar_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('notificationcommand', Notificationcommand::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsTo('customvar_flat', CustomvarFlat::class) ->setCandidateKey('customvar_id') ->setForeignKey('customvar_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/NotificationcommandEnvvar.php000066400000000000000000000035011516124654200261200ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $notificationcommand_id * @property string $envvar_key * @property string $environment_id * @property string $properties_checksum * @property string $envvar_value */ class NotificationcommandEnvvar extends Model { public function getTableName() { return 'notificationcommand_envvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'notificationcommand_id', 'envvar_key', 'environment_id', 'properties_checksum', 'envvar_value' ]; } public function getColumnDefinitions() { return [ 'notificationcommand_id' => t('Notificationcommand Id'), 'envvar_key' => t('Notificationcommand Envvar Key'), 'environment_id' => t('Environment Id'), 'properties_checksum' => t('Notificationcommand Envvar Properties Checksum'), 'envvar_value' => t('Notificationcommand Envvar Value') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'notificationcommand_id', 'environment_id', 'properties_checksum', ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('notificationcommand', Notificationcommand::class); } } icingadb-web-1.4.0/library/Icingadb/Model/RedundancyGroup.php000066400000000000000000000050071516124654200240650ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use ipl\Orm\Defaults; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Query; use ipl\Orm\Relations; /** * Redundancy group model. * * @property string $id * @property string $environment_id * @property string $display_name * * @property (?RedundancyGroupState)|Query $state * @property DependencyEdge|Query $from * @property DependencyEdge|Query $to * * @property RedundancyGroupSummary $summary */ class RedundancyGroup extends Model { use Auth; public function getTableName(): string { return 'redundancy_group'; } public function getKeyName(): string { return 'id'; } public function getColumns(): array { return [ 'environment_id', 'display_name' ]; } public function getColumnDefinitions(): array { return [ 'display_name' => t('Redundancy Group Display Name') ]; } public function createBehaviors(Behaviors $behaviors): void { $behaviors->add(new Binary([ 'id', 'environment_id' ])); $behaviors->add(new ReRoute([ 'child' => 'to.from', 'parent' => 'from.to' ])); } public function createRelations(Relations $relations): void { $relations->hasOne('state', RedundancyGroupState::class) ->setJoinType('LEFT'); $relations->hasOne('dependency_node', DependencyNode::class)->setJoinType('LEFT'); $relations->belongsToMany('from', DependencyEdge::class) ->setTargetCandidateKey('from_node_id') ->setTargetForeignKey('id') ->through(DependencyNode::class); $relations->belongsToMany('to', DependencyEdge::class) ->setTargetCandidateKey('to_node_id') ->setTargetForeignKey('id') ->through(DependencyNode::class); } public function createDefaults(Defaults $defaults): void { $defaults->add('summary', function (RedundancyGroup $group) { $summary = RedundancyGroupSummary::for($group->id, Backend::getDb()); $this->applyRestrictions($summary); return $summary->first(); }); } } icingadb-web-1.4.0/library/Icingadb/Model/RedundancyGroupState.php000066400000000000000000000047731516124654200250770ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use Icinga\Module\Icingadb\Common\Icons; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\BoolCast; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Query; use ipl\Orm\Relations; use ipl\Web\Widget\Icon; /** * Redundancy group state model. * * @property string $id * @property string $environment_id * @property string $redundancy_group_id * @property bool $failed * @property bool $is_reachable * @property DateTime $last_state_change * * @property RedundancyGroup|Query $redundancy_group */ class RedundancyGroupState extends Model { public function getTableName(): string { return 'redundancy_group_state'; } public function getKeyName(): string { return 'id'; } public function getColumns(): array { return [ 'environment_id', 'redundancy_group_id', 'failed', 'is_reachable', 'last_state_change' ]; } public function getColumnDefinitions() { return [ 'failed' => t('Redundancy Group Failed'), 'is_reachable' => t('Redundancy Group Is Reachable'), 'last_state_change' => t('Redundancy Group Last State Change') ]; } public function createBehaviors(Behaviors $behaviors): void { $behaviors->add(new Binary([ 'id', 'environment_id', 'redundancy_group_id' ])); $behaviors->add(new BoolCast([ 'failed', 'is_reachable' ])); $behaviors->add(new MillisecondTimestamp([ 'last_state_change' ])); } public function createRelations(Relations $relations): void { $relations->belongsTo('redundancy_group', RedundancyGroup::class); } /** * Get the state text for the redundancy group state * * Do not use this method to label the state of a redundancy group. * * @return string */ public function getStateText(): string { return $this->failed ? 'critical' : 'ok'; } /** * Get the state icon * * @return ?Icon */ public function getIcon(): ?Icon { if (! $this->is_reachable) { return new Icon(Icons::UNREACHABLE); } return null; } } icingadb-web-1.4.0/library/Icingadb/Model/RedundancyGroupSummary.php000066400000000000000000000154531516124654200254510ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use InvalidArgumentException; use ipl\Orm\Query; use ipl\Sql\Connection; use ipl\Sql\Expression; use ipl\Stdlib\Filter; /** * Redundancy group's summary * * @property int $nodes_total * @property int $nodes_ok * @property int $nodes_problem_handled * @property int $nodes_problem_unhandled * @property int $nodes_pending * @property int $nodes_unknown_handled * @property int $nodes_unknown_unhandled * @property int $nodes_warning_handled * @property int $nodes_warning_unhandled * @property int $nodes_acknowledged * @property int $nodes_problems_unacknowledged */ class RedundancyGroupSummary extends DependencyNode { public function getSummaryColumns(): array { return [ 'nodes_total' => new Expression('COUNT(*)'), 'nodes_ok' => new Expression( 'SUM(CASE' . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 0 THEN 1 ELSE 0 END)' . ' WHEN %s = 0 THEN 1' . ' ELSE 0' . ' END)', [ 'service_id', 'service.state.soft_state', 'host.state.soft_state', ] ), 'nodes_problem_handled' => new Expression( 'SUM(CASE' . " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 2 AND (%s = 'y' OR %s = 'n') THEN 1 ELSE 0 END)" . " WHEN %s = 1 AND (%s = 'y' OR %s = 'n') THEN 1" . ' ELSE 0' . ' END)', [ 'service_id', 'service.state.soft_state', 'service.state.is_handled', 'service.state.is_reachable', 'host.state.soft_state', 'host.state.is_handled', 'host.state.is_reachable', ] ), 'nodes_problem_unhandled' => new Expression( 'SUM(CASE' . " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 2 AND (%s = 'n' AND %s = 'y') THEN 1 ELSE 0 END)" . " WHEN %s = 1 AND (%s = 'n' AND %s = 'y') THEN 1" . ' ELSE 0' . ' END)', [ 'service_id', 'service.state.soft_state', 'service.state.is_handled', 'service.state.is_reachable', 'host.state.soft_state', 'host.state.is_handled', 'host.state.is_reachable', ] ), 'nodes_pending' => new Expression( 'SUM(CASE' . ' WHEN %s IS NOT NULL THEN (CASE WHEN %s = 99 THEN 1 ELSE 0 END)' . ' WHEN %s = 99 THEN 1' . ' ELSE 0' . ' END)', [ 'service_id', 'service.state.soft_state', 'host.state.soft_state', ] ), 'nodes_unknown_handled' => new Expression( 'SUM(CASE' . " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 3 AND (%s = 'y' OR %s = 'n') THEN 1 ELSE 0 END)" . ' ELSE 0' . ' END)', [ 'service_id', 'service.state.soft_state', 'service.state.is_handled', 'service.state.is_reachable' ] ), 'nodes_unknown_unhandled' => new Expression( 'SUM(CASE' . " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 3 AND (%s = 'n' AND %s = 'y') THEN 1 ELSE 0 END)" . ' ELSE 0' . ' END)', [ 'service_id', 'service.state.soft_state', 'service.state.is_handled', 'service.state.is_reachable' ] ), 'nodes_warning_handled' => new Expression( 'SUM(CASE' . " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 1 AND (%s = 'y' OR %s = 'n') THEN 1 ELSE 0 END)" . ' ELSE 0' . ' END)', [ 'service_id', 'service.state.soft_state', 'service.state.is_handled', 'service.state.is_reachable' ] ), 'nodes_warning_unhandled' => new Expression( 'SUM(CASE' . " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 1 AND (%s = 'n' AND %s = 'y') THEN 1 ELSE 0 END)" . ' ELSE 0' . ' END)', [ 'service_id', 'service.state.soft_state', 'service.state.is_handled', 'service.state.is_reachable' ] ), 'nodes_acknowledged' => new Expression( 'SUM(CASE' . " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 'y' THEN 1 ELSE 0 END)" . " WHEN %s = 'y' THEN 1" . ' ELSE 0' . ' END)', [ 'service_id', 'service.state.is_acknowledged', 'host.state.is_acknowledged', ] ), 'nodes_problems_unacknowledged' => new Expression( 'SUM(CASE' . " WHEN %s IS NOT NULL THEN (CASE WHEN %s = 'y' AND %s = 'n' THEN 1 ELSE 0 END)" . " WHEN %s = 'y' AND %s = 'n' THEN 1" . ' ELSE 0' . ' END)', [ 'service_id', 'service.state.is_problem', 'service.state.is_acknowledged', 'host.state.is_problem', 'host.state.is_acknowledged', ] ) ]; } public static function on(Connection $db): Query { $q = parent::on($db); /** @var static $m */ $m = $q->getModel(); $q->columns($m->getSummaryColumns()); return $q; } /** * Get the summary query for the given redundancy group id * * @param string $groupId The redundancy group id for summary * @param Connection $db Db connection to use * * @return Query */ public static function for(string $groupId, Connection $db): Query { return self::on($db) ->filter(Filter::equal('child.redundancy_group.id', $groupId)); } public function getColumns(): array { return array_merge(parent::getColumns(), $this->getSummaryColumns()); } public function getDefaultSort(): array { return []; } } icingadb-web-1.4.0/library/Icingadb/Model/Schema.php000066400000000000000000000017771516124654200221660ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; /** * Schema model * * @property string $id * @property int $version * @property DateTime $timestamp */ class Schema extends Model { public function getTableName(): string { return 'icingadb_schema'; } public function getKeyName(): string { return 'id'; } public function getColumns(): array { return [ 'version', 'timestamp' ]; } public function getDefaultSort(): array { return ['timestamp desc']; } public function createBehaviors(Behaviors $behaviors): void { $behaviors->add(new Binary(['id'])); $behaviors->add(new MillisecondTimestamp(['timestamp'])); } } icingadb-web-1.4.0/library/Icingadb/Model/Service.php000066400000000000000000000256131516124654200223610ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Backend; use ipl\Orm\Behavior\BoolCast; use Icinga\Module\Icingadb\Model\Behavior\HasProblematicParent; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use ipl\Orm\Defaults; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; use ipl\Orm\ResultSet; /** * @property string $id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $host_id * @property string $name * @property string $name_ci * @property string $display_name * @property string $checkcommand_name * @property string $checkcommand_id * @property int $max_check_attempts * @property string $check_timeperiod_name * @property ?string $check_timeperiod_id * @property ?int $check_timeout * @property int $check_interval * @property int $check_retry_interval * @property bool $active_checks_enabled * @property bool $passive_checks_enabled * @property bool $event_handler_enabled * @property bool $notifications_enabled * @property bool $flapping_enabled * @property float $flapping_threshold_low * @property float $flapping_threshold_high * @property bool $perfdata_enabled * @property string $eventcommand_name * @property ?string $eventcommand_id * @property bool $is_volatile * @property ?string $action_url_id * @property ?string $notes_url_id * @property string $notes * @property ?string $icon_image_id * @property string $icon_image_alt * @property string $zone_name * @property ?string $zone_id * @property string $command_endpoint_name * @property ?string $command_endpoint_id * @property ?int $total_children */ class Service extends Model { use Auth; public function getTableName() { return 'service'; } public function getKeyName() { return 'id'; } public function getColumns() { $columns = [ 'environment_id', 'name_checksum', 'properties_checksum', 'host_id', 'name', 'name_ci', 'display_name', 'checkcommand_name', 'checkcommand_id', 'max_check_attempts', 'check_timeperiod_name', 'check_timeperiod_id', 'check_timeout', 'check_interval', 'check_retry_interval', 'active_checks_enabled', 'passive_checks_enabled', 'event_handler_enabled', 'notifications_enabled', 'flapping_enabled', 'flapping_threshold_low', 'flapping_threshold_high', 'perfdata_enabled', 'eventcommand_name', 'eventcommand_id', 'is_volatile', 'action_url_id', 'notes_url_id', 'notes', 'icon_image_id', 'icon_image_alt', 'zone_name', 'zone_id', 'command_endpoint_name', 'command_endpoint_id' ]; if (Backend::supportsDependencies()) { $columns[] = 'total_children'; } return $columns; } public function getColumnDefinitions() { $columns = [ 'environment_id' => t('Environment Id'), 'name_checksum' => t('Service Name Checksum'), 'properties_checksum' => t('Service Properties Checksum'), 'host_id' => t('Host Id'), 'name' => t('Service Name'), 'name_ci' => t('Service Name (CI)'), 'display_name' => t('Service Display Name'), 'checkcommand_name' => t('Checkcommand Name'), 'checkcommand_id' => t('Checkcommand Id'), 'max_check_attempts' => t('Service Max Check Attempts'), 'check_timeperiod_name' => t('Check Timeperiod Name'), 'check_timeperiod_id' => t('Check Timeperiod Id'), 'check_timeout' => t('Service Check Timeout'), 'check_interval' => t('Service Check Interval'), 'check_retry_interval' => t('Service Check Retry Interval'), 'active_checks_enabled' => t('Service Active Checks Enabled'), 'passive_checks_enabled' => t('Service Passive Checks Enabled'), 'event_handler_enabled' => t('Service Event Handler Enabled'), 'notifications_enabled' => t('Service Notifications Enabled'), 'flapping_enabled' => t('Service Flapping Enabled'), 'flapping_threshold_low' => t('Service Flapping Threshold Low'), 'flapping_threshold_high' => t('Service Flapping Threshold High'), 'perfdata_enabled' => t('Service Performance Data Enabled'), 'eventcommand_name' => t('Eventcommand Name'), 'eventcommand_id' => t('Eventcommand Id'), 'is_volatile' => t('Service Is Volatile'), 'action_url_id' => t('Action Url Id'), 'notes_url_id' => t('Notes Url Id'), 'notes' => t('Service Notes'), 'icon_image_id' => t('Icon Image Id'), 'icon_image_alt' => t('Icon Image Alt'), 'zone_name' => t('Zone Name'), 'zone_id' => t('Zone Id'), 'command_endpoint_name' => t('Endpoint Name'), 'command_endpoint_id' => t('Endpoint Id'), ]; if (Backend::supportsDependencies()) { $columns['total_children'] = t('Total Children'); } return $columns; } public function getSearchColumns() { return ['name_ci', 'display_name']; } public function getDefaultSort() { return 'service.display_name'; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new BoolCast([ 'active_checks_enabled', 'passive_checks_enabled', 'event_handler_enabled', 'notifications_enabled', 'flapping_enabled', 'is_volatile' ])); $behaviors->add(new ReRoute([ 'child' => 'to.from', 'parent' => 'from.to', 'user' => 'notification.user', 'usergroup' => 'notification.usergroup' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'name_checksum', 'properties_checksum', 'host_id', 'checkcommand_id', 'check_timeperiod_id', 'eventcommand_id', 'action_url_id', 'notes_url_id', 'icon_image_id', 'zone_id', 'command_endpoint_id' ])); if (Backend::supportsDependencies()) { $behaviors->add(new HasProblematicParent()); } } public function createDefaults(Defaults $defaults) { $defaults->add('vars', function (self $subject) { if (! $subject->customvar_flat instanceof ResultSet) { $this->applyRestrictions($subject->customvar_flat); } $vars = []; foreach ($subject->customvar_flat as $customVar) { $vars[$customVar->flatname] = $customVar->flatvalue; } return $vars; }); $defaults->add('customvars', function (self $subject) { // In contrast to customvar_flat, we cannot apply restrictions here as denylists and protections // require the flattened values to work. To retrieve only customvars the user has access to, the // `vars` property should be used instead. $vars = []; foreach ($subject->customvar as $customVar) { $vars[$customVar->name] = json_decode($customVar->value, true); } return $vars; }); } public function createRelations(Relations $relations) { $relations->hasOne('state', ServiceState::class)->setJoinType('LEFT'); $relations->hasOne('dependency_node', DependencyNode::class)->setJoinType('LEFT'); $relations->hasOne('unreachable_parent', UnreachableParent::class)->setJoinType('LEFT'); $relations->belongsTo('environment', Environment::class); $relations->belongsTo('host', Host::class)->setJoinType('LEFT'); $relations->belongsTo('checkcommand', Checkcommand::class); $relations->belongsTo('timeperiod', Timeperiod::class) ->setCandidateKey('check_timeperiod_id') ->setJoinType('LEFT'); $relations->belongsTo('eventcommand', Eventcommand::class); $relations->belongsTo('action_url', ActionUrl::class) ->setCandidateKey('action_url_id') ->setForeignKey('id'); $relations->belongsTo('notes_url', NotesUrl::class) ->setCandidateKey('notes_url_id') ->setForeignKey('id'); $relations->belongsTo('icon_image', IconImage::class) ->setCandidateKey('icon_image_id') ->setJoinType('LEFT'); $relations->belongsTo('zone', Zone::class); $relations->belongsTo('endpoint', Endpoint::class) ->setCandidateKey('command_endpoint_id'); $relations->belongsToMany('customvar', Customvar::class) ->through(ServiceCustomvar::class) ->setThroughAlias('t_service_customvar'); $relations->belongsToMany('customvar_flat', CustomvarFlat::class) ->through(ServiceCustomvar::class); $relations->belongsToMany('vars', Vars::class) ->through(ServiceCustomvar::class); $relations->belongsToMany('servicegroup', Servicegroup::class) ->through(ServicegroupMember::class); $relations->belongsToMany('hostgroup', Hostgroup::class) ->through(HostgroupMember::class); $relations->hasMany('comment', Comment::class)->setJoinType('LEFT'); $relations->hasMany('downtime', Downtime::class)->setJoinType('LEFT'); $relations->hasMany('history', History::class); $relations->hasMany('notification', Notification::class)->setJoinType('LEFT'); $relations->hasMany('notification_history', NotificationHistory::class); $relations->belongsToMany('from', DependencyEdge::class) ->setTargetCandidateKey('from_node_id') ->setTargetForeignKey('id') ->through(DependencyNode::class); $relations->belongsToMany('to', DependencyEdge::class) ->setTargetCandidateKey('to_node_id') ->setTargetForeignKey('id') ->through(DependencyNode::class); } } icingadb-web-1.4.0/library/Icingadb/Model/ServiceCustomvar.php000066400000000000000000000025661516124654200242670ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $service_id * @property string $customvar_id * @property string $environment_id */ class ServiceCustomvar extends Model { public function getTableName() { return 'service_customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'service_id', 'customvar_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'service_id', 'customvar_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('service', Service::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsTo('customvar_flat', CustomvarFlat::class) ->setForeignKey('customvar_id') ->setCandidateKey('customvar_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/ServiceState.php000066400000000000000000000072511516124654200233600ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Common\ServiceStates; use ipl\Orm\Relations; /** * Service state model. * * @property string $service_id */ class ServiceState extends State { public function getTableName() { return 'service_state'; } public function getKeyName() { return 'service_id'; } public function getColumnDefinitions() { $columns = [ 'environment_id' => t('Environment Id'), 'state_type' => t('Service State Type'), 'soft_state' => t('Service Soft State'), 'hard_state' => t('Service Hard State'), 'previous_soft_state' => t('Service Previous Soft State'), 'previous_hard_state' => t('Service Previous Hard State'), 'check_attempt' => t('Service Check Attempt No.'), 'severity' => t('Service State Severity'), 'output' => t('Service Output'), 'long_output' => t('Service Long Output'), 'performance_data' => t('Service Performance Data'), 'normalized_performance_data' => t('Service Normalized Performance Data'), 'check_commandline' => t('Service Check Commandline'), 'is_problem' => t('Service Has Problem'), 'is_handled' => t('Service Is Handled'), 'is_reachable' => t('Service Is Reachable'), 'is_flapping' => t('Service Is Flapping'), 'is_overdue' => t('Service Check Is Overdue'), 'is_acknowledged' => t('Service Is Acknowledged'), 'acknowledgement_comment_id' => t('Acknowledgement Comment Id'), 'in_downtime' => t('Service In Downtime'), 'execution_time' => t('Service Check Execution Time'), 'latency' => t('Service Check Latency'), 'check_timeout' => t('Service Check Timeout'), 'check_source' => t('Service Check Source'), 'scheduling_source' => t('Service Scheduling Source'), 'last_update' => t('Service Last Update'), 'last_state_change' => t('Service Last State Change'), 'next_check' => t('Service Next Check'), 'next_update' => t('Service Next Update') ]; if (Backend::supportsDependencies()) { $columns['affects_children'] = t('Service Affects Children'); $columns['is_sticky_acknowledgement'] = t('Acknowledgement Is Sticky'); } return $columns; } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('service', Service::class); $relations->hasOne('last_comment', LastServiceComment::class) ->setCandidateKey('last_comment_id') ->setForeignKey('id') ->setJoinType('LEFT'); } public function getStateText(): string { return ServiceStates::text($this->soft_state); } public function getStateTextTranslated(): string { return ServiceStates::translated($this->soft_state); } } icingadb-web-1.4.0/library/Icingadb/Model/Servicegroup.php000066400000000000000000000056171516124654200234400ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $display_name * @property ?string $zone_id */ class Servicegroup extends Model { public function getTableName() { return 'servicegroup'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'display_name', 'zone_id' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'name_checksum' => t('Servicegroup Name Checksum'), 'properties_checksum' => t('Servicegroup Properties Checksum'), 'name' => t('Servicegroup Name'), 'name_ci' => t('Servicegroup Name (CI)'), 'display_name' => t('Servicegroup Display Name'), 'zone_id' => t('Zone Id') ]; } public function getSearchColumns() { return ['name_ci', 'display_name']; } public function getDefaultSort() { return 'display_name'; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new ReRoute([ 'host' => 'service.host', 'hostgroup' => 'service.hostgroup', 'parent' => 'service.from.to', 'child' => 'service.to.from' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'name_checksum', 'properties_checksum', 'zone_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->belongsToMany('customvar', Customvar::class) ->through(ServicegroupCustomvar::class) ->setThroughAlias('t_servicegroup_customvar'); $relations->belongsToMany('customvar_flat', CustomvarFlat::class) ->through(ServicegroupCustomvar::class); $relations->belongsToMany('vars', Vars::class) ->through(ServicegroupCustomvar::class); $relations->belongsToMany('service', Service::class) ->through(ServicegroupMember::class); } } icingadb-web-1.4.0/library/Icingadb/Model/ServicegroupCustomvar.php000066400000000000000000000026311516124654200253350ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $servicegroup_id * @property string $customvar_id * @property string $environment_id */ class ServicegroupCustomvar extends Model { public function getTableName() { return 'servicegroup_customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'servicegroup_id', 'customvar_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'servicegroup_id', 'customvar_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('servicegroup', Servicegroup::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsTo('customvar_flat', CustomvarFlat::class) ->setCandidateKey('customvar_id') ->setForeignKey('customvar_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/ServicegroupMember.php000066400000000000000000000023501516124654200245570ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $service_id * @property string $servicegroup_id * @property string $environment_id */ class ServicegroupMember extends Model { public function getTableName() { return 'servicegroup_member'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'service_id', 'servicegroup_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'service_id', 'servicegroup_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('servicegroup', Servicegroup::class); $relations->belongsTo('service', Service::class); } } icingadb-web-1.4.0/library/Icingadb/Model/ServicegroupSummary.php000066400000000000000000000160311516124654200250060ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Common\Auth; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use ipl\Orm\Query; use ipl\Orm\Relations; use ipl\Orm\UnionModel; use ipl\Orm\UnionQuery; use ipl\Sql\Adapter\Pgsql; use ipl\Sql\Connection; use ipl\Sql\Expression; use ipl\Sql\Select; /** * @property string $id * @property string $display_name * @property string $name_ci * @property string $name * @property int $services_critical_handled * @property int $services_critical_unhandled * @property int $services_ok * @property int $services_pending * @property int $services_total * @property int $services_unknown_handled * @property int $services_unknown_unhandled * @property int $services_warning_handled * @property int $services_warning_unhandled * @property int $services_severity */ class ServicegroupSummary extends UnionModel { public static function on(Connection $db): UnionQuery { $q = parent::on($db); $q->on( Query::ON_SELECT_ASSEMBLED, function () use ($q) { $auth = new class () { use Auth; }; $auth->assertColumnRestrictions($q->getFilter()); } ); $q->on($q::ON_SELECT_ASSEMBLED, function (Select $select) use ($q) { $model = $q->getModel(); $groupBy = $q->getResolver()->qualifyColumnsAndAliases((array) $model->getKeyName(), $model, false); // For PostgreSQL, ALL non-aggregate SELECT columns must appear in the GROUP BY clause: if ($q->getDb()->getAdapter() instanceof Pgsql) { /** * Ignore Expressions, i.e. aggregate functions {@see getColumns()}, * which do not need to be added to the GROUP BY. */ $candidates = array_filter($select->getColumns(), 'is_string'); // Remove already considered columns for the GROUP BY, i.e. the primary key. $candidates = array_diff_assoc($candidates, $groupBy); $groupBy = array_merge($groupBy, $candidates); } $select->groupBy($groupBy); }); return $q; } public function getTableName() { return 'servicegroup'; } public function getKeyName() { return ['id' => 'servicegroup_id']; } public function getColumns() { return [ 'name' => 'servicegroup_name', 'name_ci' => 'servicegroup_name_ci', 'display_name' => 'servicegroup_display_name', 'services_critical_handled' => new Expression( 'SUM(CASE WHEN service_state = 2' . ' AND (service_handled = \'y\' OR service_reachable = \'n\') THEN 1 ELSE 0 END)' ), 'services_critical_unhandled' => new Expression( 'SUM(CASE WHEN service_state = 2' . ' AND service_handled = \'n\' AND service_reachable = \'y\' THEN 1 ELSE 0 END)' ), 'services_ok' => new Expression( 'SUM(CASE WHEN service_state = 0 THEN 1 ELSE 0 END)' ), 'services_pending' => new Expression( 'SUM(CASE WHEN service_state = 99 THEN 1 ELSE 0 END)' ), 'services_total' => new Expression( 'SUM(CASE WHEN service_id IS NOT NULL THEN 1 ELSE 0 END)' ), 'services_unknown_handled' => new Expression( 'SUM(CASE WHEN service_state = 3' . ' AND (service_handled = \'y\' OR service_reachable = \'n\') THEN 1 ELSE 0 END)' ), 'services_unknown_unhandled' => new Expression( 'SUM(CASE WHEN service_state = 3' . ' AND service_handled = \'n\' AND service_reachable = \'y\' THEN 1 ELSE 0 END)' ), 'services_warning_handled' => new Expression( 'SUM(CASE WHEN service_state = 1' . ' AND (service_handled = \'y\' OR service_reachable = \'n\') THEN 1 ELSE 0 END)' ), 'services_warning_unhandled' => new Expression( 'SUM(CASE WHEN service_state = 1' . ' AND service_handled = \'n\' AND service_reachable = \'y\' THEN 1 ELSE 0 END)' ), 'services_severity' => new Expression('MAX(service_severity)') ]; } public function getSearchColumns() { return ['name_ci', 'display_name']; } public function getDefaultSort() { return 'display_name'; } public function getUnions(): array { $unions = [ [ Service::class, [ 'servicegroup', 'state' ], [ 'servicegroup_id' => 'servicegroup.id', 'servicegroup_name' => 'servicegroup.name', 'servicegroup_name_ci' => 'servicegroup.name_ci', 'servicegroup_display_name' => 'servicegroup.display_name', 'service_id' => 'service.id', 'service_state' => 'state.soft_state', 'service_handled' => 'state.is_handled', 'service_reachable' => 'state.is_reachable', 'service_severity' => 'state.severity' ] ], [ Servicegroup::class, [], [ 'servicegroup_id' => 'servicegroup.id', 'servicegroup_name' => 'servicegroup.name', 'servicegroup_name_ci' => 'servicegroup.name_ci', 'servicegroup_display_name' => 'servicegroup.display_name', 'service_id' => new Expression('NULL'), 'service_state' => new Expression('NULL'), 'service_handled' => new Expression('NULL'), 'service_reachable' => new Expression('NULL'), 'service_severity' => new Expression('0') ] ] ]; return $unions; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id' ])); // This is because there is no better way (new Servicegroup())->createBehaviors($behaviors); } public function createRelations(Relations $relations) { // This is because there is no better way (new Servicegroup())->createRelations($relations); } public function getColumnDefinitions() { // This is because there is no better way return (new Servicegroup())->getColumnDefinitions(); } } icingadb-web-1.4.0/library/Icingadb/Model/ServicestateSummary.php000066400000000000000000000111661516124654200247760ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Sql\Connection; use ipl\Sql\Expression; /** * @property int $services_acknowledged * @property int $services_active_checks_enabled * @property int $services_passive_checks_enabled * @property int $services_critical_handled * @property int $services_critical_unhandled * @property int $services_event_handler_enabled * @property int $services_flapping_enabled * @property int $services_notifications_enabled * @property int $services_ok * @property int $services_pending * @property int $services_problems_unacknowledged * @property int $services_total * @property int $services_unknown_handled * @property int $services_unknown_unhandled * @property int $services_warning_handled * @property int $services_warning_unhandled */ class ServicestateSummary extends Service { public function getSummaryColumns() { return [ 'services_acknowledged' => new Expression( 'SUM(CASE WHEN service_state.is_acknowledged = \'y\' THEN 1 ELSE 0 END)' ), 'services_active_checks_enabled' => new Expression( 'SUM(CASE WHEN service.active_checks_enabled = \'y\' THEN 1 ELSE 0 END)' ), 'services_passive_checks_enabled' => new Expression( 'SUM(CASE WHEN service.passive_checks_enabled = \'y\' THEN 1 ELSE 0 END)' ), 'services_critical_handled' => new Expression( 'SUM(CASE WHEN service_state.soft_state = 2' . ' AND (service_state.is_handled = \'y\' OR service_state.is_reachable = \'n\') THEN 1 ELSE 0 END)' ), 'services_critical_unhandled' => new Expression( 'SUM(CASE WHEN service_state.soft_state = 2' . ' AND service_state.is_handled = \'n\' AND service_state.is_reachable = \'y\' THEN 1 ELSE 0 END)' ), 'services_event_handler_enabled' => new Expression( 'SUM(CASE WHEN service.event_handler_enabled = \'y\' THEN 1 ELSE 0 END)' ), 'services_flapping_enabled' => new Expression( 'SUM(CASE WHEN service.flapping_enabled = \'y\' THEN 1 ELSE 0 END)' ), 'services_notifications_enabled' => new Expression( 'SUM(CASE WHEN service.notifications_enabled = \'y\' THEN 1 ELSE 0 END)' ), 'services_ok' => new Expression( 'SUM(CASE WHEN service_state.soft_state = 0 THEN 1 ELSE 0 END)' ), 'services_pending' => new Expression( 'SUM(CASE WHEN service_state.soft_state = 99 THEN 1 ELSE 0 END)' ), 'services_problems_unacknowledged' => new Expression( 'SUM(CASE WHEN service_state.is_problem = \'y\'' . ' AND service_state.is_acknowledged = \'n\' THEN 1 ELSE 0 END)' ), 'services_total' => new Expression( 'SUM(CASE WHEN service.id IS NOT NULL THEN 1 ELSE 0 END)' ), 'services_unknown_handled' => new Expression( 'SUM(CASE WHEN service_state.soft_state = 3' . ' AND (service_state.is_handled = \'y\' OR service_state.is_reachable = \'n\') THEN 1 ELSE 0 END)' ), 'services_unknown_unhandled' => new Expression( 'SUM(CASE WHEN service_state.soft_state = 3' . ' AND service_state.is_handled = \'n\' AND service_state.is_reachable = \'y\' THEN 1 ELSE 0 END)' ), 'services_warning_handled' => new Expression( 'SUM(CASE WHEN service_state.soft_state = 1' . ' AND (service_state.is_handled = \'y\' OR service_state.is_reachable = \'n\') THEN 1 ELSE 0 END)' ), 'services_warning_unhandled' => new Expression( 'SUM(CASE WHEN service_state.soft_state = 1' . ' AND service_state.is_handled = \'n\' AND service_state.is_reachable = \'y\' THEN 1 ELSE 0 END)' ) ]; } public static function on(Connection $db) { $q = parent::on($db); $q->utilize('state'); /** @var static $m */ $m = $q->getModel(); $q->columns($m->getSummaryColumns()); return $q; } public function getColumns() { return array_merge(parent::getColumns(), $this->getSummaryColumns()); } public function getDefaultSort() { return null; } public function getSearchColumns() { return ['name_ci', 'host.name_ci']; } } icingadb-web-1.4.0/library/Icingadb/Model/SlaHistoryDowntime.php000066400000000000000000000041521516124654200245640ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Relations; /** * Model for table `sla_history_downtime` * * @property string $environment_id * @property ?string $endpoint_id * @property string $object_type * @property string $host_id * @property ?string $service_id * @property string $downtime_id * @property string $downtime_start * @property string $downtime_end */ class SlaHistoryDowntime extends Model { public function getTableName() { return 'sla_history_downtime'; } public function getKeyName() { return ['host_id', 'service_id']; } public function getColumns() { return [ 'environment_id', 'endpoint_id', 'object_type', 'host_id', 'service_id', 'downtime_id', 'downtime_start', 'downtime_end' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new MillisecondTimestamp([ 'downtime_start', 'downtime_end' ])); $behaviors->add(new Binary([ 'environment_id', 'endpoint_id', 'host_id', 'service_id', 'downtime_id' ])); $behaviors->add(new ReRoute([ 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('endpoint', Endpoint::class); $relations->belongsTo('environment', Environment::class); $relations->belongsTo('host', Host::class)->setJoinType('LEFT'); $relations->belongsTo('service', Service::class)->setJoinType('LEFT'); $relations->belongsTo('downtime', Downtime::class)->setJoinType('LEFT'); } } icingadb-web-1.4.0/library/Icingadb/Model/SlaHistoryState.php000066400000000000000000000037351516124654200240640ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Relations; /** * Model for table `sla_history_state` * * @property string $environment_id * @property ?string $endpoint_id * @property string $object_type * @property string $host_id * @property ?string $service_id * @property string $downtime_id * @property string $downtime_start * @property string $downtime_end */ class SlaHistoryState extends Model { public function getTableName() { return 'sla_history_state'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'endpoint_id', 'object_type', 'host_id', 'service_id', 'event_time', 'hard_state', 'previous_hard_state' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new MillisecondTimestamp([ 'event_time' ])); $behaviors->add(new Binary([ 'environment_id', 'endpoint_id', 'host_id', 'service_id', 'downtime_id' ])); $behaviors->add(new ReRoute([ 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('endpoint', Endpoint::class); $relations->belongsTo('environment', Environment::class); $relations->belongsTo('host', Host::class)->setJoinType('LEFT'); $relations->belongsTo('service', Service::class)->setJoinType('LEFT'); } } icingadb-web-1.4.0/library/Icingadb/Model/State.php000066400000000000000000000151521516124654200220360ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Common\Icons; use ipl\Orm\Behavior\BoolCast; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Web\Widget\Icon; /** * Base class for the {@link HostState} and {@link ServiceState} models providing common columns. * * @property string $id * @property string $environment_id The environment id * @property string $state_type The state type (hard or soft) * @property int $soft_state The current soft state code (0 = OK, 1 = WARNING, 2 = CRITICAL, 3 = UNKNOWN) * @property int $hard_state The current hard state code (0 = OK, 1 = WARNING, 2 = CRITICAL, 3 = UNKNOWN) * @property int $previous_soft_state The previous soft state code (0 = OK, 1 = WARNING, 2 = CRITICAL, 3 = UNKNOWN) * @property int $previous_hard_state The previous hard state code (0 = OK, 1 = WARNING, 2 = CRITICAL, 3 = UNKNOWN) * @property int $check_attempt The check attempt count * @property int $severity The calculated severity * @property ?string $output The check output * @property ?string $long_output The long check output * @property ?string $performance_data The performance data * @property ?string $normalized_performance_data The normalized performance data (converted ms to s, GiB to byte etc.) * @property ?string $check_commandline The executed check command * @property bool $is_problem Whether in non-OK state * @property bool $is_handled Whether the state is handled * @property bool $is_reachable Whether the node is reachable * @property bool $is_flapping Whether the state is flapping * @property bool $is_overdue Whether the check is overdue * @property bool $is_acknowledged Whether the state is acknowledged * @property bool $is_sticky_acknowledgement Whether the acknowledgement is sticky * @property ?string $acknowledgement_comment_id The id of acknowledgement comment * @property ?string $last_comment_id The id of last comment * @property bool $in_downtime Whether the node is in downtime * @property ?int $execution_time The check execution time * @property ?int $latency The check latency * @property ?int $check_timeout The check timeout * @property ?string $check_source The name of the node that executes the check * @property ?string $scheduling_source The name of the node that schedules the check * @property ?DateTime $last_update The time when the node was last updated * @property DateTime $last_state_change The time when the node last got a status change * @property DateTime $next_check The time when the node will execute the next check * @property DateTime $next_update The time when the next check of the node is expected to end * @property bool $affects_children Whether any of the children is affected if there is a problem */ abstract class State extends Model { /** * Get the state as the textual representation * * @return string */ abstract public function getStateText(): string; /** * Get the state as the translated textual representation * * @return string */ abstract public function getStateTextTranslated(): string; public function getColumns() { $columns = [ 'environment_id', 'state_type', 'soft_state', 'hard_state', 'previous_soft_state', 'previous_hard_state', 'check_attempt', 'severity', 'output', 'long_output', 'performance_data', 'normalized_performance_data', 'check_commandline', 'is_problem', 'is_handled', 'is_reachable', 'is_flapping', 'is_overdue', 'is_acknowledged', 'acknowledgement_comment_id', 'last_comment_id', 'in_downtime', 'execution_time', 'latency', 'check_timeout', 'check_source', 'scheduling_source', 'last_update', 'last_state_change', 'next_check', 'next_update' ]; if (Backend::supportsDependencies()) { $columns[] = 'affects_children'; $columns[] = 'is_sticky_acknowledgement'; } return $columns; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new BoolCast([ 'is_problem', 'is_handled', 'is_reachable', 'is_flapping', 'is_overdue', 'is_acknowledged', 'is_sticky_acknowledgement', 'in_downtime', 'affects_children' ])); $behaviors->add(new MillisecondTimestamp([ 'last_update', 'last_state_change', 'next_check', 'next_update' ])); $behaviors->add(new Binary([ $this->getKeyName(), 'environment_id', 'acknowledgement_comment_id', 'last_comment_id' ])); } /** * Get the state icon * * @return Icon|null */ public function getIcon(): ?Icon { $icon = null; switch (true) { case $this->is_acknowledged: $icon = new Icon(Icons::IS_ACKNOWLEDGED, [ 'title' => sprintf('%s (%s)', strtoupper($this->getStateTextTranslated()), t('is acknowledged')) ]); break; case $this->in_downtime: $icon = new Icon(Icons::IN_DOWNTIME, [ 'title' => sprintf( '%s (%s)', strtoupper($this->getStateTextTranslated()), $this->is_handled ? t('handled by Downtime') : t('in Downtime') ) ]); break; case $this->is_flapping: $icon = new Icon(Icons::IS_FLAPPING, [ 'title' => sprintf('%s (%s)', strtoupper($this->getStateTextTranslated()), t('is flapping')) ]); break; case ! $this->is_reachable: $icon = new Icon(Icons::HOST_DOWN, [ 'title' => sprintf('%s (%s)', strtoupper($this->getStateTextTranslated()), t('is unreachable')) ]); break; case $this->is_handled: $icon = new Icon(Icons::HOST_DOWN); break; } return $icon; } } icingadb-web-1.4.0/library/Icingadb/Model/StateHistory.php000066400000000000000000000073331516124654200234220ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use DateTime; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behavior\MillisecondTimestamp; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * Model for table `state_history` * * Please note that using this model will fetch history entries for decommissioned services. To avoid this, * the query needs a `state_history.service_id IS NULL OR state_history_service.id IS NOT NULL` where. * * @property string $id * @property string $environment_id * @property ?string $endpoint_id * @property string $object_type * @property string $host_id * @property ?string $service_id * @property DateTime $event_time * @property string $state_type * @property int $soft_state * @property int $hard_state * @property int $check_attempt * @property int $previous_soft_state * @property int $previous_hard_state * @property ?string $output * @property ?string $long_output * @property int $max_check_attempts * @property ?string $check_source * @property ?string $scheduling_source */ class StateHistory extends Model { public function getTableName() { return 'state_history'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'endpoint_id', 'object_type', 'host_id', 'service_id', 'event_time', 'state_type', 'soft_state', 'hard_state', 'check_attempt', 'previous_soft_state', 'previous_hard_state', 'output', 'long_output', 'max_check_attempts', 'check_source', 'scheduling_source' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'endpoint_id' => t('Endpoint Id'), 'object_type' => t('Object Type'), 'host_id' => t('Host Id'), 'service_id' => t('Service Id'), 'event_time' => t('Event Time'), 'state_type' => t('Event State Type'), 'soft_state' => t('Event Soft State'), 'hard_state' => t('Event Hard State'), 'check_attempt' => t('Event Check Attempt No.'), 'previous_soft_state' => t('Event Previous Soft State'), 'previous_hard_state' => t('Event Previous Hard State'), 'output' => t('Event Output'), 'long_output' => t('Event Long Output'), 'max_check_attempts' => t('Event Max Check Attempts'), 'check_source' => t('Event Check Source') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new MillisecondTimestamp([ 'event_time' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'endpoint_id', 'host_id', 'service_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('endpoint', Endpoint::class); $relations->belongsTo('environment', Environment::class); $relations->belongsTo('history', History::class) ->setCandidateKey('id') ->setForeignKey('state_history_id'); $relations->belongsTo('host', Host::class); $relations->belongsTo('service', Service::class)->setJoinType('LEFT'); } } icingadb-web-1.4.0/library/Icingadb/Model/Timeperiod.php000066400000000000000000000061321516124654200230550ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $display_name * @property string $prefer_includes * @property ?string $zone_id */ class Timeperiod extends Model { public function getTableName() { return 'timeperiod'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'display_name', 'prefer_includes', 'zone_id' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'name_checksum' => t('Timeperiod Name Checksum'), 'properties_checksum' => t('Timeperiod Properties Checksum'), 'name' => t('Timeperiod Name'), 'name_ci' => t('Timeperiod Name (CI)'), 'display_name' => t('Timeperiod Display Name'), 'prefer_includes' => t('Timeperiod Prefer Includes'), 'zone_id' => t('Zone Id') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new ReRoute([ 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'name_checksum', 'properties_checksum', 'zone_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->belongsToMany('customvar', Customvar::class) ->through(TimeperiodCustomvar::class) ->setThroughAlias('t_timeperiod_customvar'); $relations->belongsToMany('customvar_flat', CustomvarFlat::class) ->through(TimeperiodCustomvar::class); $relations->belongsToMany('vars', Vars::class) ->through(TimeperiodCustomvar::class); // TODO: Decide how to establish the override relations $relations->hasMany('range', TimeperiodRange::class); $relations->hasMany('host', Host::class) ->setForeignKey('check_timeperiod_id'); $relations->hasMany('Notification', Notification::class); $relations->hasMany('service', Service::class) ->setForeignKey('check_timeperiod_id'); $relations->hasMany('user', User::class); } } icingadb-web-1.4.0/library/Icingadb/Model/TimeperiodCustomvar.php000066400000000000000000000026131516124654200247610ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $timeperiod_id * @property string $customvar_id * @property string $environment_id */ class TimeperiodCustomvar extends Model { public function getTableName() { return 'timeperiod_customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'timeperiod_id', 'customvar_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'timeperiod_id', 'customvar_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('timeperiod', Timeperiod::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsTo('customvar_flat', CustomvarFlat::class) ->setCandidateKey('customvar_id') ->setForeignKey('customvar_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/TimeperiodOverrideExclude.php000066400000000000000000000025521516124654200260710ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $timeperiod_id * @property string $override_id * @property string $environment_id */ class TimeperiodOverrideExclude extends Model { public function getTableName() { return 'timeperiod_override_exclude'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'timeperiod_id', 'override_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'timeperiod_id', 'override_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('timeperiod', Timeperiod::class); // TODO: `timeperiod` cannot be used again, find a better name $relations->belongsTo('timeperiod', Timeperiod::class) ->setCandidateKey('override_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/TimeperiodOverrideInclude.php000066400000000000000000000025521516124654200260630ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $timeperiod_id * @property string $override_id * @property string $environment_id */ class TimeperiodOverrideInclude extends Model { public function getTableName() { return 'timeperiod_override_include'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'timeperiod_id', 'override_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'timeperiod_id', 'override_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('timeperiod', Timeperiod::class); // TODO: `timeperiod` cannot be used again, find a better name $relations->belongsTo('timeperiod', Timeperiod::class) ->setCandidateKey('override_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/TimeperiodRange.php000066400000000000000000000030051516124654200240260ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $timeperiod_id * @property string $range_key * @property string $environment_id * @property string $range_value */ class TimeperiodRange extends Model { public function getTableName() { return 'timeperiod_range'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'timeperiod_id', 'range_key', 'environment_id', 'range_value' ]; } public function getColumnDefinitions() { return [ 'timeperiod_id' => t('Timeperiod Id'), 'range_key' => t('Timeperiod Range Date(s)/Day'), 'environment_id' => t('Environment Id'), 'range_value' => t('Timeperiod Range Time') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'timeperiod_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('timeperiod', Timeperiod::class); } } icingadb-web-1.4.0/library/Icingadb/Model/UnreachableParent.php000066400000000000000000000166221516124654200243440ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use Icinga\Module\Icingadb\Model\UnreachableParent\ResultSet; use InvalidArgumentException; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Query; use ipl\Orm\Relations; use ipl\Sql\Adapter\Pgsql; use ipl\Sql\Connection; use ipl\Sql\Expression; use ipl\Sql\Select; use ipl\Stdlib\Filter; /** * Class UnreachableParent * * @property string $id * @property int $level * @property string $child_id * @property ?string $host_id * @property ?string $service_id * @property ?string $redundancy_group_id * @property int $is_group_member * * @property (?Host)|Query $host * @property (?Service)|Query $service * @property (?RedundancyGroup)|Query $redundancy_group */ class UnreachableParent extends DependencyNode { public function getTableName(): string { return 'unreachable_parent'; } public function getKeyName(): string { return 'id'; } public function getColumns(): array { return [ 'id', 'child_id', 'level', 'host_id', 'service_id', 'redundancy_group_id', 'is_group_member', '_has_problematic_parent' ]; } public function createRelations(Relations $relations): void { $relations->belongsTo('host', Host::class) ->setJoinType('LEFT'); $relations->belongsTo('service', Service::class) ->setJoinType('LEFT'); $relations->belongsTo('redundancy_group', RedundancyGroup::class) ->setJoinType('LEFT'); } public function createBehaviors(Behaviors $behaviors): void { $behaviors->add(new Binary([ 'id', 'child_id', 'host_id', 'service_id', 'redundancy_group_id' ])); $behaviors->add(new ReRoute([ 'hostgroup' => 'host.hostgroup', 'servicegroup' => 'service.servicegroup' ])); } public static function on(Connection $db, ?Model $root = null): Query { if ($root === null) { throw new InvalidArgumentException('Root node must not be null'); } $query = parent::on($db); $query->getSelectBase()->with( self::selectNodes($db, $root), 'unreachable_parent', true ); $query->filter(Filter::all( Filter::greaterThan('level', 0), Filter::equal('is_group_member', 0), Filter::any( Filter::any( Filter::equal('service.state.affects_children', 'y'), Filter::all( // TODO: Remove this once implicit dependencies are properly implemented Filter::equal('_has_problematic_parent', 0), Filter::equal('service.state.is_reachable', 'n') ) ), Filter::all( Filter::unlike('service_id', '*'), Filter::equal('host.state.affects_children', 'y') ), Filter::all( Filter::equal('redundancy_group.state.failed', 'y'), Filter::equal('redundancy_group.state.is_reachable', 'y') ) ) )); $query->setResultSetClass(ResultSet::class); return $query; } private static function selectNodes(Connection $db, Model $root): Select { if ($db->getAdapter() instanceof Pgsql) { $binaryCast = "CAST('00000000000000000000' AS bytea20)"; $booleanCast = "CAST('0' AS boolean)"; } else { $binaryCast = "CAST('' AS binary(20))"; $booleanCast = '0'; } $rootQuery = DependencyNode::on($db) ->columns([ 'id' => 'id', 'child_id' => 'id', 'level' => new Expression('0'), 'host_id' => 'host_id', 'service_id' => new Expression("COALESCE(%s, $binaryCast)", ['service_id']), 'redundancy_group_id' => new Expression($binaryCast), 'is_group_member' => new Expression($booleanCast), '_has_problematic_parent' => new Expression($booleanCast) ]) ->disableDefaultSort(); if ($root instanceof Host) { $rootQuery->filter(Filter::all( Filter::equal('host_id', $root->id), Filter::unlike('service_id', '*') )); } elseif ($root instanceof Service) { $rootQuery->filter(Filter::all( Filter::equal('host_id', $root->host_id), Filter::equal('service_id', $root->id) )); } elseif ($root instanceof RedundancyGroup) { $rootQuery->filter(Filter::all(Filter::equal('redundancy_group_id', $root->id))); } else { throw new InvalidArgumentException('Root node must be either a host, service or a redundancy group'); } $nodeQuery = DependencyEdge::on($db) ->columns([ 'id' => 'to_node_id', 'child_id' => 'from_node_id', 'level' => new Expression('urn.level + 1'), 'host_id' => 'to.host_id', 'service_id' => 'to.service_id', 'redundancy_group_id' => 'to.redundancy_group_id', 'is_group_member' => new Expression('urn.redundancy_group_id IS NOT NULL AND urn.level > 0'), '_has_problematic_parent' => new Expression(sprintf( 'EXISTS(SELECT 1 FROM %s immediate_parents' . ' INNER JOIN %s state ON state.id = immediate_parents.dependency_edge_state_id' . ' WHERE immediate_parents.from_node_id = %%s AND state.failed = \'y\')', (new DependencyEdge())->getTableName(), (new DependencyEdgeState())->getTableName() ), ['to_node_id']) ]); $nodeQuery->filter(Filter::equal('state.failed', 'y')); $nodeSelect = $nodeQuery->assembleSelect(); // TODO: ipl-orm doesn't preserve key order :'( $columnsProperty = (new \ReflectionClass($nodeSelect))->getProperty('columns'); $columnsProperty->setValue($nodeSelect, array_merge( [ 'id' => null, 'child_id' => null, 'level' => null, 'host_id' => null, 'service_id' => null, 'redundancy_group_id' => null, 'is_group_member' => null, '_has_problematic_parent' => null ], $nodeSelect->getColumns() )); return $rootQuery->assembleSelect()->union( $nodeSelect ->join(['urn' => 'unreachable_parent'], sprintf( 'urn.id = %s', $nodeQuery ->getResolver() ->qualifyColumn( 'from_node_id', $nodeQuery ->getResolver() ->getAlias($nodeQuery->getModel()) ) )) ); } } icingadb-web-1.4.0/library/Icingadb/Model/UnreachableParent/000077500000000000000000000000001516124654200236245ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Model/UnreachableParent/ResultSet.php000066400000000000000000000011761516124654200262740ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model\UnreachableParent; use Generator; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Traversable; class ResultSet extends VolatileStateResults { protected function yieldTraversable(Traversable $traversable): Generator { $knownIds = []; foreach ($traversable as $value) { if (isset($knownIds[$value->id])) { continue; } $knownIds[$value->id] = true; yield $value; } } } icingadb-web-1.4.0/library/Icingadb/Model/User.php000066400000000000000000000113331516124654200216710ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\Bitmask; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $display_name * @property string $email * @property string $pager * @property string $notifications_enabled * @property ?string $timeperiod_id * @property string[] $states * @property string[] $types * @property ?string $zone_id */ class User extends Model { public function getTableName() { return 'user'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'display_name', 'email', 'pager', 'notifications_enabled', 'timeperiod_id', 'states', 'types', 'zone_id' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'name_checksum' => t('Contact Name Checksum'), 'properties_checksum' => t('Contact Properties Checksum'), 'name' => t('Contact Name'), 'name_ci' => t('Contact Name (CI)'), 'display_name' => t('Contact Display Name'), 'email' => t('Contact Email'), 'pager' => t('Contact Pager'), 'notifications_enabled' => t('Contact Receives Notifications'), 'timeperiod_id' => t('Timeperiod Id'), 'states' => t('Notification State Filter'), 'types' => t('Notification Type Filter'), 'zone_id' => t('Zone Id') ]; } public function getSearchColumns() { return ['name_ci', 'display_name']; } public function getDefaultSort() { return 'user.display_name'; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new ReRoute([ 'host' => 'notification.host', 'service' => 'notification.service', 'hostgroup' => 'notification.host.hostgroup', 'servicegroup' => 'notification.service.servicegroup' ])); $behaviors->add(new Bitmask([ 'states' => [ 'ok' => 1, 'warning' => 2, 'critical' => 4, 'unknown' => 8, 'up' => 16, 'down' => 32 ], 'types' => [ 'downtime_start' => 1, 'downtime_end' => 2, 'downtime_removed' => 4, 'custom' => 8, 'ack' => 16, 'problem' => 32, 'recovery' => 64, 'flapping_start' => 128, 'flapping_end' => 256 ] ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'name_checksum', 'properties_checksum', 'timeperiod_id', 'zone_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('timeperiod', Timeperiod::class)->setJoinType('LEFT'); $relations->belongsTo('zone', Zone::class); $relations->belongsToMany('customvar', Customvar::class) ->through(UserCustomvar::class) ->setThroughAlias('t_user_customvar'); $relations->belongsToMany('customvar_flat', CustomvarFlat::class) ->through(UserCustomvar::class); $relations->belongsToMany('vars', Vars::class) ->through(UserCustomvar::class); $relations->belongsToMany('notification', Notification::class) ->through('notification_recipient'); $relations->belongsToMany('notification_history', NotificationHistory::class) ->through('user_notification_history'); $relations->belongsToMany('usergroup', Usergroup::class) ->through(UsergroupMember::class); } } icingadb-web-1.4.0/library/Icingadb/Model/UserCustomvar.php000066400000000000000000000025411516124654200235760ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $customvar_id * @property string $user_id * @property string $environment_id */ class UserCustomvar extends Model { public function getTableName() { return 'user_customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'user_id', 'customvar_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'user_id', 'customvar_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('user', User::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsTo('customvar_flat', CustomvarFlat::class) ->setCandidateKey('customvar_id') ->setForeignKey('customvar_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/Usergroup.php000066400000000000000000000060541516124654200227520ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $display_name * @property ?string $zone_id */ class Usergroup extends Model { public function getTableName() { return 'usergroup'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'display_name', 'zone_id' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'name_checksum' => t('Contactgroup Name Checksum'), 'properties_checksum' => t('Contactgroup Properties Checksum'), 'name' => t('Contactgroup Name'), 'name_ci' => t('Contactgroup Name (CI)'), 'display_name' => t('Contactgroup Display Name'), 'zone_id' => t('Zone Id') ]; } public function getSearchColumns() { return ['name_ci', 'display_name']; } public function getDefaultSort() { return 'usergroup.display_name'; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new ReRoute([ 'host' => 'notification.host', 'service' => 'notification.service', 'hostgroup' => 'notification.host.hostgroup', 'servicegroup' => 'notification.service.servicegroup' ])); $behaviors->add(new Binary([ 'id', 'environment_id', 'name_checksum', 'properties_checksum', 'zone_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->belongsToMany('customvar', Customvar::class) ->through(UsergroupCustomvar::class) ->setThroughAlias('t_usergroup_customvar'); $relations->belongsToMany('customvar_flat', CustomvarFlat::class) ->through(UsergroupCustomvar::class); $relations->belongsToMany('vars', Vars::class) ->through(UsergroupCustomvar::class); $relations->belongsToMany('user', User::class) ->through(UsergroupMember::class); $relations->belongsToMany('notification', Notification::class) ->through('notification_recipient'); } } icingadb-web-1.4.0/library/Icingadb/Model/UsergroupCustomvar.php000066400000000000000000000026041516124654200246530ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $usergroup_id * @property string $customvar_id * @property string $environment_id */ class UsergroupCustomvar extends Model { public function getTableName() { return 'usergroup_customvar'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'usergroup_id', 'customvar_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'usergroup_id', 'customvar_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('usergroup', Usergroup::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsTo('customvar_flat', CustomvarFlat::class) ->setCandidateKey('customvar_id') ->setForeignKey('customvar_id'); } } icingadb-web-1.4.0/library/Icingadb/Model/UsergroupMember.php000066400000000000000000000023041516124654200240740ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $usergroup_id * @property string $user_id * @property string $environment_id */ class UsergroupMember extends Model { public function getTableName() { return 'usergroup_member'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'user_id', 'usergroup_id', 'environment_id' ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'user_id', 'usergroup_id', 'environment_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('usergroup', Usergroup::class); $relations->belongsTo('user', User::class); } } icingadb-web-1.4.0/library/Icingadb/Model/Vars.php000066400000000000000000000014541516124654200216710ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use Icinga\Module\Icingadb\Model\Behavior\FlattenedObjectVars; use ipl\Orm\Behaviors; use ipl\Sql\Connection; class Vars extends CustomvarFlat { /** * @internal Don't use. This model acts only as relation target and is not supposed to be directly used as query * target. Use {@see CustomvarFlat} instead. */ public static function on(Connection $_) { throw new \LogicException('Documentation says: DO NOT USE. Can\'t you read?'); } public function createBehaviors(Behaviors $behaviors) { parent::createBehaviors($behaviors); $behaviors->add(new FlattenedObjectVars()); } } icingadb-web-1.4.0/library/Icingadb/Model/Zone.php000066400000000000000000000052661516124654200216760ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Model; use ipl\Orm\Behavior\Binary; use ipl\Orm\Behaviors; use Icinga\Module\Icingadb\Common\Model; use ipl\Orm\Relations; /** * @property string $id * @property string $environment_id * @property string $name_checksum * @property string $properties_checksum * @property string $name * @property string $name_ci * @property string $is_global * @property ?string $parent_id * @property int $depth */ class Zone extends Model { public function getTableName() { return 'zone'; } public function getKeyName() { return 'id'; } public function getColumns() { return [ 'environment_id', 'name_checksum', 'properties_checksum', 'name', 'name_ci', 'is_global', 'parent_id', 'depth' ]; } public function getColumnDefinitions() { return [ 'environment_id' => t('Environment Id'), 'name_checksum' => t('Zone Name Checksum'), 'properties_checksum' => t('Zone Properties Checksum'), 'name' => t('Zone Name'), 'name_ci' => t('Zone Name (CI)'), 'is_global' => t('Zone Is Global'), 'parent_id' => t('Parent Zone Id'), 'depth' => t('Zone Depth') ]; } public function createBehaviors(Behaviors $behaviors) { $behaviors->add(new Binary([ 'id', 'environment_id', 'name_checksum', 'properties_checksum', 'parent_id' ])); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->hasMany('comment', Comment::class); $relations->hasMany('downtime', Downtime::class); $relations->hasMany('endpoint', Endpoint::class); $relations->hasMany('eventcommand', Eventcommand::class); $relations->hasMany('host', Host::class); $relations->hasMany('hostgroup', Hostgroup::class); $relations->hasMany('notification', Notification::class); $relations->hasMany('service', Service::class); $relations->hasMany('servicegroup', Servicegroup::class); $relations->hasMany('timeperiod', Timeperiod::class); $relations->hasMany('user', User::class); $relations->hasMany('usergroup', Usergroup::class); // TODO: Decide how to establish recursive relations } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/000077500000000000000000000000001516124654200215765ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/ProvidedHook/ApplicationState.php000066400000000000000000000073471516124654200255660ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook; use Exception; use Icinga\Application\Hook\ApplicationStateHook; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Common\IcingaRedis; use Icinga\Module\Icingadb\Model\Instance; use Icinga\Web\Session; use ipl\Stdlib\Filter; class ApplicationState extends ApplicationStateHook { use Database; public function collectMessages() { try { $lastIcingaHeartbeat = IcingaRedis::getLastIcingaHeartbeat(); } catch (Exception $e) { $downSince = Session::getSession()->getNamespace('icingadb')->get('redis.down-since'); if ($downSince === null) { $downSince = time(); Session::getSession()->getNamespace('icingadb')->set('redis.down-since', $downSince); } $this->addError( 'icingadb/redis-down', $downSince, sprintf(t("Can't connect to Redis: %s"), $e->getMessage()) ); return; } $instance = Instance::on($this->getDb()) ->with(['endpoint']) ->filter(Filter::equal('responsible', true)) ->orderBy('heartbeat', 'desc') ->first(); if ($instance === null) { $noInstanceSince = Session::getSession() ->getNamespace('icingadb')->get('icingadb.no-instance-since'); if ($noInstanceSince === null) { $noInstanceSince = time(); Session::getSession() ->getNamespace('icingadb')->set('icingadb.no-instance-since', $noInstanceSince); } $this->addError( 'icingadb/no-instance', $noInstanceSince, t( 'It seems that Icinga DB is not running.' . ' Make sure Icinga DB is running and writing into the database.' ) ); return; } else { Session::getSession()->getNamespace('icingadb')->delete('db.no-instance-since'); } $outdatedDbHeartbeat = $instance->heartbeat->getTimestamp() < time() - 60; if ($lastIcingaHeartbeat === null) { $missingSince = Session::getSession() ->getNamespace('icingadb')->get('redis.heartbeat-missing-since'); if ($missingSince === null) { $missingSince = time(); Session::getSession() ->getNamespace('icingadb')->set('redis.heartbeat-missing-since', $missingSince); } $lastIcingaHeartbeat = $missingSince; } else { Session::getSession()->getNamespace('icingadb')->delete('redis.heartbeat-missing-since'); } switch (true) { case $outdatedDbHeartbeat && $instance->heartbeat->getTimestamp() > $lastIcingaHeartbeat: $this->addError( 'icingadb/redis-outdated', $lastIcingaHeartbeat, t('Redis is outdated. Make sure Icinga 2 is running and connected to Redis.') ); break; case $outdatedDbHeartbeat: $this->addError( 'icingadb/icingadb-down', $instance->heartbeat->getTimestamp(), t( 'It seems that Icinga DB is not running.' . ' Make sure Icinga DB is running and writing into the database.' ) ); break; } Session::getSession()->getNamespace('icingadb')->delete('redis.down-since'); } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/CreateHostSlaReport.php000066400000000000000000000021341516124654200262040ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook; use Icinga\Authentication\Auth; use Icinga\Module\Icingadb\Hook\HostActionsHook; use Icinga\Module\Icingadb\Model\Host; use ipl\I18n\Translation; use ipl\Stdlib\Filter; use ipl\Web\Filter\QueryString; use ipl\Web\Url; use ipl\Web\Widget\Link; class CreateHostSlaReport extends HostActionsHook { use Translation; public function getActionsForObject(Host $host): array { if (! Auth::getInstance()->hasPermission('reporting/reports')) { return []; } $filter = QueryString::render(Filter::equal('host.name', $host->name)); return [ new Link( $this->translate('Create Host SLA Report'), Url::fromPath('reporting/reports/new')->addParams(['filter' => $filter, 'report' => 'host']), [ 'data-icinga-modal' => true, 'data-no-icinga-ajax' => true ] ) ]; } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/CreateHostsSlaReport.php000066400000000000000000000024151516124654200263710ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook; use Icinga\Authentication\Auth; use Icinga\Module\Icingadb\Hook\HostsDetailExtensionHook; use ipl\Html\Html; use ipl\Html\HtmlDocument; use ipl\Html\ValidHtml; use ipl\I18n\Translation; use ipl\Orm\Query; use ipl\Web\Filter\QueryString; use ipl\Web\Url; use ipl\Web\Widget\Link; class CreateHostsSlaReport extends HostsDetailExtensionHook { use Translation; public function getHtmlForObjects(Query $hosts): ValidHtml { if (Auth::getInstance()->hasPermission('reporting/reports')) { $filter = QueryString::render($this->getBaseFilter()); return (new HtmlDocument()) ->addHtml(Html::tag('h2', $this->translate('Reporting'))) ->addHtml(new Link( $this->translate('Create Host SLA Report'), Url::fromPath('reporting/reports/new')->addParams(['filter' => $filter, 'report' => 'host']), [ 'data-icinga-modal' => true, 'data-no-icinga-ajax' => true ] )); } return new HtmlDocument(); } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/CreateServiceSlaReport.php000066400000000000000000000023331516124654200266700ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook; use Icinga\Authentication\Auth; use Icinga\Module\Icingadb\Hook\ServiceActionsHook; use Icinga\Module\Icingadb\Model\Service; use ipl\I18n\Translation; use ipl\Stdlib\Filter; use ipl\Web\Filter\QueryString; use ipl\Web\Url; use ipl\Web\Widget\Link; class CreateServiceSlaReport extends ServiceActionsHook { use Translation; public function getActionsForObject(Service $service): array { if (! Auth::getInstance()->hasPermission('reporting/reports')) { return []; } $filter = QueryString::render(Filter::all( Filter::equal('service.name', $service->name), Filter::equal('host.name', $service->host->name) )); return [ new Link( $this->translate('Create Service SLA Report'), Url::fromPath('reporting/reports/new')->addParams(['filter' => $filter, 'report' => 'service']), [ 'data-icinga-modal' => true, 'data-no-icinga-ajax' => true ] ) ]; } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/CreateServicesSlaReport.php000066400000000000000000000024361516124654200270570ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook; use Icinga\Authentication\Auth; use Icinga\Module\Icingadb\Hook\ServicesDetailExtensionHook; use ipl\Html\Html; use ipl\Html\HtmlDocument; use ipl\Html\ValidHtml; use ipl\I18n\Translation; use ipl\Orm\Query; use ipl\Web\Filter\QueryString; use ipl\Web\Url; use ipl\Web\Widget\Link; class CreateServicesSlaReport extends ServicesDetailExtensionHook { use Translation; public function getHtmlForObjects(Query $services): ValidHtml { if (Auth::getInstance()->hasPermission('reporting/reports')) { $filter = QueryString::render($this->getBaseFilter()); return (new HtmlDocument()) ->addHtml(Html::tag('h2', $this->translate('Reporting'))) ->addHtml(new Link( $this->translate('Create Service SLA Report'), Url::fromPath('reporting/reports/new')->addParams(['filter' => $filter, 'report' => 'service']), [ 'data-icinga-modal' => true, 'data-no-icinga-ajax' => true ] )); } return new HtmlDocument(); } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/IcingaHealth.php000066400000000000000000000132551516124654200246350ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook; use Icinga\Application\Hook\HealthHook; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Model\Instance; use ipl\Web\Url; class IcingaHealth extends HealthHook { use Database; public const REQUIRED_ICINGADB_VERSION = '1.4.0'; /** * normalizeVersion extracts a version string from Icinga DB's reported * version comparable with REQUIRED_ICINGADB_VERSION via version_compare. * * @param string $version Icinga DB version string to normalize. * * @return string Normalized Icinga DB version string. */ public static function normalizeVersion(string $version): string { // Ignore leading "v" when the git tag is used, e.g., "v1.4.0". $version = ltrim($version, 'v'); // Ignore everything after "-" used by git describe, e.g., "1.4.0-g...". $version = explode('-', $version, 2)[0]; return $version; } /** @var Instance */ protected $instance; public function getName(): string { return 'Icinga DB'; } public function getUrl(): Url { return Url::fromPath('icingadb/health'); } public function checkHealth() { $instance = $this->getInstance(); if ($instance === null) { $this->setState(self::STATE_UNKNOWN); $this->setMessage(t( 'Icinga DB is not running or not writing into the database' . ' (make sure the icinga feature "icingadb" is enabled)' )); } elseif ($instance->heartbeat->getTimestamp() < time() - 60) { $this->setState(self::STATE_CRITICAL); $this->setMessage(t( 'Icinga DB is not running or not writing into the database' . ' (make sure the icinga feature "icingadb" is enabled)' )); } elseif ( ! isset($instance->icingadb_version) || version_compare( self::normalizeVersion($instance->icingadb_version), self::REQUIRED_ICINGADB_VERSION, '<' ) ) { $this->setState(self::STATE_CRITICAL); $this->setMessage(sprintf( t('Icinga DB is outdated, please upgrade to version %s or later.'), self::REQUIRED_ICINGADB_VERSION )); } else { $this->setState(self::STATE_OK); $warningMessages = []; if (! $instance->icinga2_active_host_checks_enabled) { $this->setState(self::STATE_WARNING); $warningMessages[] = t('Active host checks are disabled'); } if (! $instance->icinga2_active_service_checks_enabled) { $this->setState(self::STATE_WARNING); $warningMessages[] = t('Active service checks are disabled'); } if (! $instance->icinga2_notifications_enabled) { $this->setState(self::STATE_WARNING); $warningMessages[] = t('Notifications are disabled'); } if ($this->getState() === self::STATE_WARNING) { $this->setMessage(implode("; ", $warningMessages)); } else { $this->setMessage(sprintf( t('Icinga DB is running and writing into the database. (Version: %s)'), $instance->icingadb_version )); } } if ($instance !== null) { $this->setMetrics([ 'heartbeat' => $instance->heartbeat->getTimestamp(), 'responsible' => $instance->responsible, 'icinga2_active_host_checks_enabled' => $instance->icinga2_active_host_checks_enabled, 'icinga2_active_service_checks_enabled' => $instance->icinga2_active_service_checks_enabled, 'icinga2_event_handlers_enabled' => $instance->icinga2_event_handlers_enabled, 'icinga2_flap_detection_enabled' => $instance->icinga2_flap_detection_enabled, 'icinga2_notifications_enabled' => $instance->icinga2_notifications_enabled, 'icinga2_performance_data_enabled' => $instance->icinga2_performance_data_enabled, 'icinga2_start_time' => $instance->icinga2_start_time->getTimestamp(), 'icinga2_version' => $instance->icinga2_version, 'icingadb_version' => $instance->icingadb_version ?? null, 'endpoint' => ['name' => $instance->endpoint->name] ]); } } /** * Get an Icinga DB instance * * @return ?Instance */ protected function getInstance() { if ($this->instance === null) { $query = Instance::on($this->getDb()) ->with('endpoint') ->columns([ 'heartbeat', 'responsible', 'icinga2_active_host_checks_enabled', 'icinga2_active_service_checks_enabled', 'icinga2_event_handlers_enabled', 'icinga2_flap_detection_enabled', 'icinga2_notifications_enabled', 'icinga2_performance_data_enabled', 'icinga2_start_time', 'icinga2_version', 'endpoint.name' ]); if (Backend::supportsDependencies()) { $query->withColumns('icingadb_version'); } $this->instance = $query->first(); } return $this->instance; } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/Notifications/000077500000000000000000000000001516124654200244075ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/ProvidedHook/Notifications/V1/000077500000000000000000000000001516124654200246755ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/ProvidedHook/Notifications/V1/Source.php000066400000000000000000000172561516124654200266610ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook\Notifications\V1; use Icinga\Application\Logger; use Icinga\Exception\ConfigurationError; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Notifications\Hook\V1\SourceHook; use InvalidArgumentException; use ipl\Html\Attributes; use ipl\Html\Contract\Form; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Sql\Expression; use ipl\Stdlib\Filter; use ipl\Stdlib\Filter\Condition; use ipl\Web\Control\SearchBar\SearchException; use ipl\Web\Control\SearchEditor; use ipl\Web\Filter\QueryString; use ipl\Web\Filter\Renderer; use ipl\Web\Url; use ipl\Web\Widget\IcingaIcon; use ipl\Web\Widget\Icon; use JsonException; class Source implements SourceHook { use Translation; /** @var string */ public const TYPE_ALL = 'all'; /** @var array */ private array $allowedColumns; public function __construct() { $this->allowedColumns = [ 'host.name' => $this->translate('Host Name'), 'hostgroup.name' => $this->translate('Hostgroup Name'), 'host.user.name' => $this->translate('Contact Name'), 'host.usergroup.name' => $this->translate('Contactgroup Name'), 'service.name' => $this->translate('Service Name'), 'servicegroup.name' => $this->translate('Servicegroup Name'), 'service.user.name' => $this->translate('Contact Name'), 'service.usergroup.name' => $this->translate('Contactgroup Name') ]; } public function getSourceType(): string { return 'icinga2'; } public function getSourceLabel(): string { return 'Icinga'; } public function getSourceIcon(): Icon { return new IcingaIcon('icinga'); } public function getRuleFilterTargets(int $sourceId): array { return [ 'host' => $this->translate('Hosts only'), 'service' => $this->translate('Services only'), self::TYPE_ALL => $this->translate('Hosts and Services') ]; } public function getRuleFilterEditor(string $filter): SearchEditor { if ($filter === 'host' || $filter === 'service' || $filter === self::TYPE_ALL) { $type = $filter; $filter = ''; } else { try { $data = json_decode($filter, true, flags: JSON_THROW_ON_ERROR); } catch (JsonException $e) { Logger::error('Failed to parse rule filter configuration: %s (Error: %s)', $filter, $e); throw new ConfigurationError($this->translate( 'Failed to parse rule filter configuration. Please contact your system administrator.' )); } if ($data['version'] !== 1 || ! isset($data['config']['type']) || ! isset($data['config']['filter'])) { Logger::error('Invalid rule filter configuration: %s', $filter); throw new ConfigurationError($this->translate( 'Invalid rule filter configuration. Please contact your system administrator.' )); } $type = $data['config']['type']; $filter = $data['config']['filter']; } $editor = new SearchEditor(); $editor->setQueryString($filter); $editor->setSuggestionUrl(Url::fromPath( 'icingadb/suggest/restriction-column', ['_disableLayout' => true, 'showCompact' => true, 'type' => $type] )); $editor->getParser()->on(QueryString::ON_CONDITION, function (Condition $condition) { if ($condition->getColumn()) { if (array_key_exists($condition->getColumn(), $this->allowedColumns)) { $condition->metaData()->set('columnLabel', $this->allowedColumns[$condition->getColumn()]); } elseif (preg_match('/^(host|service)\.vars\.(.*)/i', $condition->getColumn(), $m)) { $prefix = $m[1] === 'host' ? $this->translate('Host') : $this->translate('Service'); $condition->metaData()->set('columnLabel', $prefix . ' ' . $m[2]); } } }); $editor->on(SearchEditor::ON_VALIDATE_COLUMN, function (Condition $condition) { if (! array_key_exists($condition->getColumn(), $this->allowedColumns)) { if (! preg_match('/^(?:host|service)\.vars\./i', $condition->getColumn())) { throw new SearchException($this->translate('Is not a valid column')); } } })->on(HtmlDocument::ON_ASSEMBLED, function (SearchEditor $editor) use ($type) { $editor->prependHtml(new HtmlElement( 'p', Attributes::create(['class' => 'description']), Text::create( match ($type) { 'host' => $this->translate( 'Only hosts matching the following criteria will be affected.' ), 'service' => $this->translate( 'Only services matching the following criteria will be affected.' ), self::TYPE_ALL => $this->translate( 'All hosts and services matching the following criteria will be affected.' ) } ) )); // Not using addElement, as otherwise the submit button is hidden because it's not last-of-type $hidden = $editor->createElement('hidden', 'type', ['value' => $type]); $editor->registerElement($hidden); $editor->prependHtml($hidden); }); return $editor; } public function serializeRuleFilter(Form $editor): string { if (! $editor instanceof SearchEditor) { throw new InvalidArgumentException('Editor must be an instance of ' . SearchEditor::class); } $rule = $editor->getFilter(); $filter = (new Renderer($rule))->render(); if ($filter === '') { return ''; } $type = $editor->getElement('type')->getValue(); $queries = []; if ($type === 'host' || $type === self::TYPE_ALL) { $queries['host'] = Host::on(Backend::getDb()) ->filter(Filter::all( Filter::equal('host.id', ':host_id'), Filter::equal('host.environment_id', ':environment_id') )); } if ($type === 'service' || $type === self::TYPE_ALL) { $queries['service'] = Service::on(Backend::getDb()) ->filter(Filter::all( Filter::equal('service.id', ':service_id'), Filter::equal('service.environment_id', ':environment_id') )); } return json_encode([ 'version' => 1, 'config' => [ 'type' => $type, 'filter' => $filter ], 'queries' => array_map(function ($query) use ($rule) { $query->columns([new Expression('1')])->filter($rule)->limit(1); [$query, $parameters] = $query->getDb()->getQueryBuilder()->assembleSelect( $query->assembleSelect()->resetOrderBy() ); return [ 'query' => $query, 'parameters' => $parameters ]; }, $queries) ], JSON_THROW_ON_ERROR); } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/RedisHealth.php000066400000000000000000000043141516124654200245050ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook; use Exception; use Icinga\Application\Hook\HealthHook; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Common\IcingaRedis; use Icinga\Module\Icingadb\Model\Instance; class RedisHealth extends HealthHook { use Database; public function getName(): string { return 'Redis'; } public function checkHealth() { try { $lastIcingaHeartbeat = IcingaRedis::getLastIcingaHeartbeat(); if ($lastIcingaHeartbeat === null) { $lastIcingaHeartbeat = time(); } $server = Backend::getRedis()->getConnection()->info('Server')['Server']; $instance = Instance::on($this->getDb())->columns('heartbeat')->first(); if ($instance === null) { $this->setState(self::STATE_UNKNOWN); $this->setMessage(t( 'Can\'t check Redis: Icinga DB is not running or not writing into the database' . ' (make sure the icinga feature "icingadb" is enabled)' )); return; } $this->setMetrics($server); $outdatedDbHeartbeat = $instance->heartbeat->getTimestamp() < time() - 60; if (! $outdatedDbHeartbeat || $instance->heartbeat->getTimestamp() <= $lastIcingaHeartbeat) { $this->setState(self::STATE_OK); $this->setMessage(sprintf( t('Redis available and in sync with Icinga. (Version: %s)'), $server['redis_version'] ?? t('N/A') )); } elseif ($instance->heartbeat->getTimestamp() > $lastIcingaHeartbeat) { $this->setState(self::STATE_CRITICAL); $this->setMessage(t('Redis outdated. Make sure Icinga 2 is running and connected to Redis.')); } } catch (Exception $e) { $this->setState(self::STATE_CRITICAL); $this->setMessage(sprintf(t("Can't connect to Redis: %s"), $e->getMessage())); } } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/Reporting/000077500000000000000000000000001516124654200235475ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php000066400000000000000000000033321516124654200270320ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook\Reporting; use Icinga\Application\Icinga; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Reporting\ReportData; use Icinga\Module\Reporting\ReportRow; use Icinga\Module\Reporting\Timerange; use ipl\Sql\Expression; use ipl\Stdlib\Filter\Rule; use function ipl\I18n\t; class HostSlaReport extends SlaReport { public function getName() { $name = t('Host SLA'); if (Icinga::app()->getModuleManager()->hasEnabled('idoreports')) { $name .= ' (Icinga DB)'; } return $name; } protected function createReportData() { return (new ReportData()) ->setDimensions([t('Hostname')]) ->setValues([t('SLA in %')]); } protected function createReportRow($row) { if ($row->sla === null) { return null; } return (new ReportRow()) ->setDimensions([$row->display_name]) ->setValues([(float) $row->sla]); } protected function fetchSla(Timerange $timerange, ?Rule $filter = null) { $sla = Host::on($this->getDb()) ->columns([ 'display_name', 'sla' => new Expression(sprintf( "get_sla_ok_percent(%s, NULL, '%s', '%s')", 'host.id', $timerange->getStart()->format('Uv'), $timerange->getEnd()->format('Uv') )) ]); $this->applyRestrictions($sla); if ($filter !== null) { $sla->filter($filter); } return $sla; } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php000066400000000000000000000036661516124654200275270ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook\Reporting; use Icinga\Application\Icinga; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Reporting\ReportData; use Icinga\Module\Reporting\ReportRow; use Icinga\Module\Reporting\Timerange; use ipl\Sql\Expression; use ipl\Stdlib\Filter\Rule; use function ipl\I18n\t; class ServiceSlaReport extends SlaReport { public function getName() { $name = t('Service SLA'); if (Icinga::app()->getModuleManager()->hasEnabled('idoreports')) { $name .= ' (Icinga DB)'; } return $name; } protected function createReportData() { return (new ReportData()) ->setDimensions([t('Hostname'), t('Service Name')]) ->setValues([t('SLA in %')]); } protected function createReportRow($row) { if ($row->sla === null) { return null; } return (new ReportRow()) ->setDimensions([$row->host->display_name, $row->display_name]) ->setValues([(float) $row->sla]); } protected function fetchSla(Timerange $timerange, ?Rule $filter = null) { $sla = Service::on($this->getDb()) ->columns([ 'host.display_name', 'display_name', 'sla' => new Expression(sprintf( "get_sla_ok_percent(%s, %s, '%s', '%s')", 'service.host_id', 'service.id', $timerange->getStart()->format('Uv'), $timerange->getEnd()->format('Uv') )) ]); $sla->resetOrderBy()->orderBy('host.display_name')->orderBy('display_name'); $this->applyRestrictions($sla); if ($filter !== null) { $sla->filter($filter); } return $sla; } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/Reporting/SlaReport.php000066400000000000000000000231761516124654200262040ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook\Reporting; use DateInterval; use DatePeriod; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Reporting\Hook\ReportHook; use Icinga\Module\Reporting\ReportData; use Icinga\Module\Reporting\ReportRow; use Icinga\Module\Reporting\Timerange; use ipl\Html\Form; use ipl\Html\Html; use ipl\Stdlib\Filter\Rule; use ipl\Web\Filter\QueryString; use ipl\Web\Widget\EmptyState; use function ipl\I18n\t; /** * Base class for host and service SLA reports */ abstract class SlaReport extends ReportHook { use Auth; use Database; /** @var float If an SLA value is lower than the threshold, it is considered not ok */ public const DEFAULT_THRESHOLD = 99.5; /** @var int The amount of decimal places for the report result */ public const DEFAULT_REPORT_PRECISION = 2; /** * Create and return a {@link ReportData} container * * @return ReportData Container initialized with the expected dimensions and value labels for the specific report */ abstract protected function createReportData(); /** * Create and return a {@link ReportRow} * * @param mixed $row Data for the row * * @return ReportRow|null Row with the dimensions and values for the specific report set according to the data * expected in {@link createRepportData()} or null for no data */ abstract protected function createReportRow($row); /** * Fetch SLA according to specified time range and filter * * @param Timerange $timerange * @param ?Rule $filter * * @return iterable */ abstract protected function fetchSla(Timerange $timerange, ?Rule $filter = null); protected function fetchReportData(Timerange $timerange, ?array $config = null) { $rd = $this->createReportData(); $rows = []; $filter = trim((string) $config['filter']) ?: '*'; $filter = $filter !== '*' ? QueryString::parse($filter) : null; $interval = null; $boundary = null; $format = null; if (isset($config['breakdown']) && $config['breakdown'] !== 'none') { switch ($config['breakdown']) { case 'hour': $interval = new DateInterval('PT1H'); $format = 'H:i:s'; $boundary = '+1 hour'; break; case 'day': $interval = new DateInterval('P1D'); $format = 'Y-m-d'; $boundary = 'tomorrow midnight'; break; case 'week': $interval = new DateInterval('P1W'); $format = 'Y-\WW'; $boundary = 'monday next week midnight'; break; case 'month': $interval = new DateInterval('P1M'); $format = 'Y-m'; $boundary = 'first day of next month midnight'; break; } $dimensions = $rd->getDimensions(); $dimensions[] = ucfirst($config['breakdown']); $rd->setDimensions($dimensions); foreach ($this->yieldTimerange($timerange, $interval, $boundary) as list($start, $end)) { foreach ($this->fetchSla(new Timerange($start, $end), $filter) as $row) { $row = $this->createReportRow($row); if ($row === null) { continue; } $dimensions = $row->getDimensions(); $dimensions[] = $start->format($format); $row->setDimensions($dimensions); $rows[] = $row; } } } else { foreach ($this->fetchSla($timerange, $filter) as $row) { $rows[] = $this->createReportRow($row); } } $rd->setRows($rows); // Disconnect from the database, as this hook usually runs isolated and no-one else requires a connection. // Makes only a difference in case of reporting triggering this as part of a scheduled report and prevents // the database connection from being left open unnecessarily and failing upon re-use. $this->getDb()->disconnect(); return $rd; } /** * Yield start and end times that recur at the specified interval over the given time range * * @param Timerange $timerange * @param DateInterval $interval * @param string|null $boundary English text datetime description for calculating bounds to get * calendar days, weeks or months instead of relative times according to interval * * @return \Generator */ protected function yieldTimerange(Timerange $timerange, DateInterval $interval, $boundary = null) { $start = clone $timerange->getStart(); $end = clone $timerange->getEnd(); $oneSecond = new DateInterval('PT1S'); if ($boundary !== null) { $intermediate = (clone $start)->modify($boundary); if ($intermediate < $end) { yield [clone $start, $intermediate->sub($oneSecond)]; $start->modify($boundary); } } $period = new DatePeriod($start, $interval, $end, DatePeriod::EXCLUDE_START_DATE); foreach ($period as $date) { /** @var \DateTime $date */ yield [$start, (clone $date)->sub($oneSecond)]; $start = $date; } yield [$start, $end]; } public function initConfigForm(Form $form) { $form->addElement('text', 'filter', [ 'label' => t('Filter') ]); $form->addElement('select', 'breakdown', [ 'label' => t('Breakdown'), 'options' => [ 'none' => t('None', 'SLA Report Breakdown'), 'hour' => t('Hour'), 'day' => t('Day'), 'week' => t('Week'), 'month' => t('Month') ] ]); $form->addElement('number', 'threshold', [ 'label' => t('Threshold'), 'placeholder' => static::DEFAULT_THRESHOLD, 'step' => '0.01', 'min' => '1', 'max' => '100' ]); $form->addElement('number', 'sla_precision', [ 'label' => t('Amount Decimal Places'), 'placeholder' => static::DEFAULT_REPORT_PRECISION, 'min' => '1', 'max' => '12' ]); $form->addElement('checkbox', 'export_total', [ 'label' => t('Export Total Averages'), 'description' => t('Export total averages to CSV and JSON'), // Instead of y/n, 0/1 can be implicitly cast to bool which is done where the config is actually used. 'checkedValue' => '1', 'uncheckedValue' => '0' ]); } public function getData(Timerange $timerange, ?array $config = null) { return $this->fetchReportData($timerange, $config); } public function getHtml(Timerange $timerange, ?array $config = null) { $data = $this->getData($timerange, $config); if (! count($data)) { return new EmptyState(t('No data found.')); } $threshold = isset($config['threshold']) ? (float) $config['threshold'] : static::DEFAULT_THRESHOLD; $tableHeaderCells = []; foreach ($data->getDimensions() as $dimension) { $tableHeaderCells[] = Html::tag('th', null, $dimension); } foreach ($data->getValues() as $value) { $tableHeaderCells[] = Html::tag('th', null, $value); } $tableRows = []; $precision = $config['sla_precision'] ?? static::DEFAULT_REPORT_PRECISION; foreach ($data->getRows() as $row) { $cells = []; foreach ($row->getDimensions() as $dimension) { $cells[] = Html::tag('td', null, $dimension); } // We only have one metric $sla = $row->getValues()[0]; if ($sla < $threshold) { $slaClass = 'nok'; } else { $slaClass = 'ok'; } $cells[] = Html::tag('td', ['class' => "sla-column $slaClass"], round($sla, $precision)); $tableRows[] = Html::tag('tr', null, $cells); } // We only have one average $average = $data->getAverages()[0]; if ($average < $threshold) { $slaClass = 'nok'; } else { $slaClass = 'ok'; } $total = $this instanceof HostSlaReport ? sprintf(t('Total (%d Hosts)'), $data->count()) : sprintf(t('Total (%d Services)'), $data->count()); $tableRows[] = Html::tag('tr', null, [ Html::tag('td', ['colspan' => count($data->getDimensions())], $total), Html::tag('td', ['class' => "sla-column $slaClass"], round($average, $precision)) ]); $table = Html::tag( 'table', ['class' => 'common-table sla-table'], [ Html::tag( 'thead', null, Html::tag( 'tr', null, $tableHeaderCells ) ), Html::tag('tbody', null, $tableRows) ] ); return $table; } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php000066400000000000000000000006461516124654200300430ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook\Reporting; use Icinga\Module\Icingadb\Hook\Common\TotalSlaReportUtils; use function ipl\I18n\t; class TotalHostSlaReport extends HostSlaReport { use TotalSlaReportUtils; public function getName() { return t('Total Host SLA'); } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php000066400000000000000000000006571516124654200305300ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook\Reporting; use Icinga\Module\Icingadb\Hook\Common\TotalSlaReportUtils; use function ipl\I18n\t; class TotalServiceSlaReport extends ServiceSlaReport { use TotalSlaReportUtils; public function getName() { return t('Total Service SLA'); } } icingadb-web-1.4.0/library/Icingadb/ProvidedHook/X509/000077500000000000000000000000001516124654200222435ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/ProvidedHook/X509/Sni.php000066400000000000000000000027331516124654200235120ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\ProvidedHook\X509; use Generator; use Icinga\Data\Filter\Filter; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\X509\Hook\SniHook; use ipl\Web\Filter\QueryString; class Sni extends SniHook { use Auth; use Database; /** * @inheritDoc */ public function getHosts(?Filter $filter = null): Generator { $this->getDb()->ping(); $queryHost = Host::on($this->getDb()) ->columns([ 'host_name' => 'name', 'host_address' => 'address', 'host_address6' => 'address6' ]); $this->applyRestrictions($queryHost); if ($filter !== null) { $queryString = $filter->toQueryString(); $filterCondition = QueryString::parse($queryString); $queryHost->filter($filterCondition); } $hosts = $this->getDb()->select($queryHost->assembleSelect()); /** @var Host $host */ foreach ($hosts as $host) { if (! empty($host->host_address)) { yield $host->host_address => $host->host_name; } if (! empty($host->host_address6)) { yield $host->host_address6 => $host->host_name; } } } } icingadb-web-1.4.0/library/Icingadb/Redis/000077500000000000000000000000001516124654200202475ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Redis/VolatileStateResults.php000066400000000000000000000166761516124654200251420ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Redis; use Icinga\Application\Benchmark; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Common\IcingaRedis; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\State; use ipl\Orm\Query; use ipl\Orm\Resolver; use ipl\Orm\ResultSet; use RuntimeException; class VolatileStateResults extends ResultSet { use Auth; /** @var Resolver */ private $resolver; /** @var bool Whether Redis is unavailable */ private $redisUnavailable; /** @var bool Whether Redis updates were applied */ private $updatesApplied = false; /** @var string Object type host */ protected const TYPE_HOST = 'host'; /** @var string Object type service */ protected const TYPE_SERVICE = 'service'; /** @var array|null Columns to be selected if they were explicitly set, if empty all columns are selected */ protected ?array $columns; /** @var bool Whether the model's ID should be contained in the results */ protected bool $includeModelID = true; public static function fromQuery(Query $query): static { $self = parent::fromQuery($query); $self->resolver = $query->getResolver(); $self->redisUnavailable = Backend::getRedis()->isUnavailable(); $self->columns = $query->getColumns(); if (! empty($self->columns)) { // The id is necessary to apply the redis-updates if ($query->getModel() instanceof Host && empty(array_intersect(['host.id', 'id'], $self->columns))) { $query->withColumns('host.id'); $self->includeModelID = false; } elseif ( $query->getModel() instanceof Service && empty(array_intersect(['service.id', 'id'], $self->columns)) ) { $query->withColumns('service.id'); $self->includeModelID = false; } } return $self; } /** * Get whether Redis is unavailable * * @return bool */ public function isRedisUnavailable(): bool { return $this->redisUnavailable; } public function current(): mixed { if (! $this->redisUnavailable && ! $this->updatesApplied && ! $this->isCacheDisabled) { $this->rewind(); } $result = parent::current(); if ($this->isCacheDisabled && ! $this->redisUnavailable) { $this->applyRedisUpdates([$result]); } if (! $this->includeModelID) { unset($result['id']); } return $result; } public function key(): int { if (! $this->redisUnavailable && ! $this->updatesApplied && ! $this->isCacheDisabled) { $this->rewind(); } return parent::key(); } public function rewind(): void { if (! $this->redisUnavailable && ! $this->updatesApplied && ! $this->isCacheDisabled) { $this->updatesApplied = true; $this->advance(); Benchmark::measure('Applying Redis updates'); $this->applyRedisUpdates($this); Benchmark::measure('Redis updates applied'); } parent::rewind(); } /** * Apply redis state details to the given results * * @param self|array $rows * * @return void */ protected function applyRedisUpdates($rows) { $type = null; $showSourceGranted = $this->getAuth()->hasPermission('icingadb/object/show-source'); $getKeysAndBehaviors = function (State $state, $type): array { $columns = array_filter($state->getColumns(), function ($column) { return ! str_ends_with($column, '_id'); }); if (! empty($this->columns)) { $normalizedColumns = array_map( fn($column) => preg_replace("/^($type\.state\.|state\.)/", '', $column), $this->columns ); $stateColumns = array_intersect($normalizedColumns, $columns); return [$stateColumns, $this->resolver->getBehaviors($state)]; } return [$columns, $this->resolver->getBehaviors($state)]; }; $states = []; foreach ($rows as $row) { if ($row instanceof DependencyNode) { if ($row->redundancy_group_id !== null) { continue; } elseif ($row->service_id !== null) { $type = self::TYPE_SERVICE; $row = $row->service; } else { $type = self::TYPE_HOST; $row = $row->host; } } elseif ($type === null) { switch (true) { case $row instanceof Host: $type = self::TYPE_HOST; break; case $row instanceof Service: $type = self::TYPE_SERVICE; break; default: throw new RuntimeException('Volatile states can only be fetched for hosts and services'); } } $states[$type][bin2hex($row->id)] = $row->state; if (! isset($states[$type]['keys'])) { [$keys, $behaviors] = $getKeysAndBehaviors($row->state, $type); if (! $showSourceGranted) { $keys = array_diff($keys, ['check_commandline']); } $states[$type]['keys'] = $keys; $states[$type]['behaviors'] = $behaviors; } if ($type === self::TYPE_SERVICE && $row->host instanceof Host && isset($row->host->id)) { $states[self::TYPE_HOST][bin2hex($row->host->id)] = $row->host->state; if (! isset($states[self::TYPE_HOST]['keys'])) { [$keys, $behaviors] = $getKeysAndBehaviors($row->host->state, $type); $states[self::TYPE_HOST]['keys'] = $keys; $states[self::TYPE_HOST]['behaviors'] = $behaviors; } } } if (! empty($states[self::TYPE_SERVICE])) { $this->apply($states[self::TYPE_SERVICE], self::TYPE_SERVICE); } if (! empty($states[self::TYPE_HOST])) { $this->apply($states[self::TYPE_HOST], self::TYPE_HOST); } } /** * Apply the given states of given type to the results * * @param array $states * @param string $type The object type ({@see self::TYPE_HOST} OR {@see self::TYPE_SERVICE}) * * @return void */ protected function apply(array $states, string $type): void { $keys = $states['keys']; $behaviors = $states['behaviors']; unset($states['keys'], $states['behaviors']); $results = $type === self::TYPE_SERVICE ? IcingaRedis::fetchServiceState(array_keys($states), $keys) : IcingaRedis::fetchHostState(array_keys($states), $keys); foreach ($results as $id => $data) { foreach ($data as $key => $value) { $data[$key] = $behaviors->retrieveProperty($value, $key); } $states[$id]->setProperties($data); } } } icingadb-web-1.4.0/library/Icingadb/Setup/000077500000000000000000000000001516124654200203015ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Setup/ApiTransportPage.php000066400000000000000000000100311516124654200242300ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Setup; use Icinga\Data\ConfigObject; use Icinga\Module\Icingadb\Command\Transport\CommandTransport; use Icinga\Module\Icingadb\Command\Transport\CommandTransportException; use Icinga\Web\Form; class ApiTransportPage extends Form { public function init() { $this->setName('setup_icingadb_api_transport'); $this->setTitle(t('Icinga 2 API')); $this->addDescription(t( 'Please fill out the connection details to the Icinga 2 API.' )); $this->setValidatePartial(true); } public function createElements(array $formData) { if (isset($formData['skip_validation']) && $formData['skip_validation']) { // In case another error occured and the checkbox was displayed before $this->addSkipValidationCheckbox(); } else { $this->addElement('hidden', 'skip_validation', ['value' => 0]); } $this->addElement('hidden', 'transport', [ 'required' => true, 'disabled' => true, 'value' => 'api' ]); $this->addElement('hidden', 'name', [ 'required' => true, 'disabled' => true, 'value' => 'icinga2' ]); $this->addElement('text', 'host', [ 'required' => true, 'label' => t('Host'), 'description' => t('Hostname or address of the Icinga master') ]); $this->addElement('number', 'port', [ 'required' => true, 'label' => t('Port'), 'value' => 5665, 'min' => 1, 'max' => 65536 ]); $this->addElement('text', 'username', [ 'required' => true, 'label' => t('API Username'), 'description' => t('User to authenticate with using HTTP Basic Auth') ]); $this->addElement('password', 'password', [ 'required' => true, 'renderPassword' => true, 'label' => t('API Password'), 'autocomplete' => 'new-password' ]); } public function isValid($formData) { if (! parent::isValid($formData)) { return false; } if (! isset($formData['skip_validation']) || ! $formData['skip_validation']) { if (! $this->validateConfiguration()) { $this->addSkipValidationCheckbox(); return false; } } return true; } public function isValidPartial(array $formData) { if (isset($formData['backend_validation']) && parent::isValid($formData)) { if (! $this->validateConfiguration()) { return false; } $this->info(t('The configuration has been successfully validated.')); } elseif (! isset($formData['backend_validation'])) { // This is usually done by isValid(Partial), but as we're not calling any of these... $this->populate($formData); } return true; } protected function validateConfiguration(): bool { try { CommandTransport::createTransport(new ConfigObject($this->getValues()))->probe(); } catch (CommandTransportException $e) { $this->error(sprintf( t('Failed to successfully validate the configuration: %s'), $e->getMessage() )); return false; } return true; } protected function addSkipValidationCheckbox() { $this->addElement( 'checkbox', 'skip_validation', [ 'ignore' => true, 'label' => t('Skip Validation'), 'description' => t('Check this to not to validate the configuration') ] ); } } icingadb-web-1.4.0/library/Icingadb/Setup/ApiTransportStep.php000066400000000000000000000057661516124654200243120ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Setup; use Exception; use Icinga\Application\Config; use Icinga\Exception\IcingaException; use Icinga\Module\Setup\Step; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Table; use ipl\Html\Text; class ApiTransportStep extends Step { /** @var array */ protected $data; /** @var Exception */ protected $error; public function __construct(array $data) { $this->data = $data; } public function apply() { $transportConfig = $this->data; $transportName = $transportConfig['name']; unset($transportConfig['name']); try { $config = Config::module('icingadb', 'commandtransports', true); $config->setSection($transportName, $transportConfig); $config->saveIni(); } catch (Exception $e) { $this->error = $e; return false; } return true; } public function getSummary() { $description = new HtmlElement('p', null, Text::create(mt( 'icingadb', 'The Icinga 2 API will be accessed using the following connection details:' ))); $apiOptions = new Table(); $apiOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Host'))), $this->data['host'] ])); $apiOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Port'))), $this->data['port'] ])); $apiOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Username'))), $this->data['username'] ])); $apiOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Password'))), str_repeat('*', strlen($this->data['password'])) ])); $topic = new HtmlElement('div', Attributes::create(['class' => 'topic'])); $topic->addHtml($description, $apiOptions); $summary = new HtmlDocument(); $summary->addHtml( new HtmlElement('h2', null, Text::create(mt('icingadb', 'Icinga 2 API'))), $topic ); return $summary->render(); } public function getReport() { if ($this->error === null) { return [sprintf( mt('icingadb', 'Commandtransport configuration update successful: %s'), Config::module('icingadb', 'commandtransports')->getConfigFile() )]; } else { return [ sprintf( mt('icingadb', 'Commandtransport configuration update failed: %s'), Config::module('icingadb', 'commandtransports')->getConfigFile() ), sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error)) ]; } } } icingadb-web-1.4.0/library/Icingadb/Setup/DbResourcePage.php000066400000000000000000000122031516124654200236420ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Setup; use Icinga\Forms\Config\ResourceConfigForm; use Icinga\Forms\Config\Resource\DbResourceForm; use Icinga\Web\Form; class DbResourcePage extends Form { public function init() { $this->setName('setup_icingadb_resource'); $this->setTitle(t('Icinga DB Resource')); $this->addDescription(t( 'Please fill out the connection details below to access Icinga DB.' )); $this->setValidatePartial(true); } public function createElements(array $formData) { $this->addElement( 'hidden', 'type', [ 'required' => true, 'disabled' => true, 'value' => 'db' ] ); if (isset($formData['skip_validation']) && $formData['skip_validation']) { // In case another error occured and the checkbox was displayed before $this->addSkipValidationCheckbox(); } else { $this->addElement('hidden', 'skip_validation', ['value' => 0]); } $dbResourceForm = new DbResourceForm(); $this->addElements($dbResourceForm->createElements($formData)->getElements()); $this->getElement('name')->setValue('icingadb'); $this->getElement('db')->setMultiOptions([ 'mysql' => 'MySQL', 'pgsql' => 'PostgreSQL' ]); $this->removeElement('name'); $this->addElement( 'hidden', 'name', [ 'required' => true, 'disabled' => true, 'value' => 'icingadb' ] ); $charsetElement = $this->getElement('charset'); if (! isset($formData['db']) || $formData['db'] === 'mysql') { $charsetElement->setValue('utf8mb4')->addDecorator( ['preserveDefault' => 'HtmlTag'], [ 'tag' => 'input', 'type' => 'hidden', 'name' => 'charset' . static::DEFAULT_SUFFIX, 'value' => 'utf8mb4' ] ); } else { $charsetElement->addDecorator( ['preserveDefault' => 'HtmlTag'], [ 'tag' => 'input', 'type' => 'hidden', 'name' => 'charset' . static::DEFAULT_SUFFIX, 'value' => '' ] ); } } public function isValid($formData) { if (! parent::isValid($formData)) { return false; } if (! isset($formData['skip_validation']) || ! $formData['skip_validation']) { if (! $this->validateConfiguration()) { $this->addSkipValidationCheckbox(); return false; } } return true; } public function isValidPartial(array $formData) { if (isset($formData['backend_validation']) && parent::isValid($formData)) { if (! $this->validateConfiguration(true)) { return false; } $this->info(t('The configuration has been successfully validated.')); } elseif (! isset($formData['backend_validation'])) { // This is usually done by isValid(Partial), but as we're not calling any of these... $this->populate($formData); } return true; } protected function validateConfiguration(bool $showLog = false): bool { $inspection = ResourceConfigForm::inspectResource($this); if ($inspection !== null) { if ($showLog) { $join = function ($e) use (&$join) { return is_string($e) ? $e : join("\n", array_map($join, $e)); }; $this->addElement( 'note', 'inspection_output', [ 'order' => 0, 'value' => '' . t('Validation Log') . "\n\n" . join("\n", array_map($join, $inspection->toArray())), 'decorators' => [ 'ViewHelper', ['HtmlTag', ['tag' => 'pre', 'class' => 'log-output']], ] ] ); } if ($inspection->hasError()) { $this->error(sprintf( t('Failed to successfully validate the configuration: %s'), $inspection->getError() )); return false; } } return true; } protected function addSkipValidationCheckbox() { $this->addElement( 'checkbox', 'skip_validation', [ 'ignore' => true, 'label' => t('Skip Validation'), 'description' => t('Check this to not to validate the configuration') ] ); } } icingadb-web-1.4.0/library/Icingadb/Setup/DbResourceStep.php000066400000000000000000000123131516124654200237030ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Setup; use Exception; use Icinga\Application\Config; use Icinga\Exception\IcingaException; use Icinga\Module\Setup\Step; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Table; use ipl\Html\Text; class DbResourceStep extends Step { /** @var array */ protected $data; /** @var Exception */ protected $error; public function __construct(array $data) { $this->data = $data; } public function apply() { $resourceConfig = $this->data; $resourceName = $resourceConfig['name']; unset($resourceConfig['name']); try { $config = Config::app('resources', true); $config->setSection($resourceName, $resourceConfig); $config->saveIni(); } catch (Exception $e) { $this->error = $e; return false; } try { $config = Config::module('icingadb', 'config', true); $config->setSection('icingadb', ['resource' => $resourceName]); $config->saveIni(); } catch (Exception $e) { $this->error = $e; return false; } return true; } public function getSummary() { $description = new HtmlElement('p', null, Text::create(mt( 'icingadb', 'Icinga DB will be accessed using the following connection details:' ))); $resourceOptions = new Table(); $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Host'))), $this->data['host'] ])); $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Port'))), $this->data['port'] ?: ($this->data['db'] === 'mysql' ? 3306 : 5432) ])); $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Database'))), $this->data['dbname'] ])); $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Username'))), $this->data['username'] ])); $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Password'))), str_repeat('*', strlen($this->data['password'])) ])); $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Charset'))), $this->data['charset'] ])); if (isset($this->data['use_ssl']) && $this->data['use_ssl']) { $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('SSL Do Not Verify Server Certificate'))), isset($this->data['ssl_do_not_verify_server_cert']) && $this->data['ssl_do_not_verify_server_cert'] ? t('Yes') : t('No') ])); $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('SSL Key'))), $this->data['ssl_key'] ?: mt('icingadb', 'None', 'non-existence of a value') ])); $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('SSL Certificate'))), $this->data['ssl_cert'] ?: mt('icingadb', 'None', 'non-existence of a value') ])); $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('SSL CA'))), $this->data['ssl_ca'] ?: mt('icingadb', 'None', 'non-existence of a value') ])); $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('The CA certificate file path'))), $this->data['ssl_capath'] ?: mt('icingadb', 'None', 'non-existence of a value') ])); $resourceOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('SSL CA Path'))), $this->data['ssl_cipher'] ?: mt('icingadb', 'None', 'non-existence of a value') ])); } $topic = new HtmlElement('div', Attributes::create(['class' => 'topic'])); $topic->addHtml($description, $resourceOptions); $summary = new HtmlDocument(); $summary->addHtml( new HtmlElement('h2', null, Text::create(mt('icingadb', 'Icinga DB Resource'))), $topic ); return $summary->render(); } public function getReport() { if ($this->error === null) { return [sprintf( mt('icingadb', 'Resource configuration update successful: %s'), Config::resolvePath('resources.ini') )]; } else { return [ sprintf( mt('icingadb', 'Resource configuration update failed: %s'), Config::resolvePath('resources.ini') ), sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error)) ]; } } } icingadb-web-1.4.0/library/Icingadb/Setup/IcingaDbWizard.php000066400000000000000000000061301516124654200236330ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Setup; use Icinga\Application\Icinga; use Icinga\Module\Setup\Forms\SummaryPage; use Icinga\Module\Setup\Requirement\PhpModuleRequirement; use Icinga\Module\Setup\Requirement\WebLibraryRequirement; use Icinga\Module\Setup\RequirementSet; use Icinga\Module\Setup\Setup; use Icinga\Module\Setup\SetupWizard; use Icinga\Web\Form; use Icinga\Web\Request; use Icinga\Web\Wizard; class IcingaDbWizard extends Wizard implements SetupWizard { protected function init() { $this->addPage(new WelcomePage()); $this->addPage(new DbResourcePage()); $this->addPage(new RedisPage()); $this->addPage(new ApiTransportPage()); $this->addPage(new SummaryPage(['name' => 'setup_icingadb_summary'])); } public function setupPage(Form $page, Request $request) { if ($page->getName() === 'setup_icingadb_summary') { $page->setSummary($this->getSetup()->getSummary()); $page->setSubjectTitle('Icinga DB Web'); } } public function getSetup() { $pageData = $this->getPageData(); $setup = new Setup(); $setup->addStep(new DbResourceStep($pageData['setup_icingadb_resource'])); $setup->addStep(new RedisStep($pageData['setup_icingadb_redis'])); $setup->addStep(new ApiTransportStep($pageData['setup_icingadb_api_transport'])); return $setup; } public function getRequirements() { $set = new RequirementSet(); $requiredVersions = Icinga::app()->getModuleManager()->getModule('icingadb')->getRequiredLibraries(); $set->add(new WebLibraryRequirement([ 'condition' => ['icinga-php-library', '', $requiredVersions['icinga-php-library']], 'alias' => 'Icinga PHP library', 'description' => t('The Icinga PHP library (IPL) is required for Icinga DB Web') ])); $set->add(new WebLibraryRequirement([ 'condition' => ['icinga-php-thirdparty', '', $requiredVersions['icinga-php-thirdparty']], 'alias' => 'Icinga PHP Thirdparty', 'description' => t('The Icinga PHP Thirdparty library is required for Icinga DB Web') ])); $set->add(new PhpModuleRequirement([ 'condition' => 'libxml', 'alias' => 'libxml', 'description' => t('For check plugins that output HTML the libxml extension is required') ])); $set->add(new PhpModuleRequirement([ 'condition' => 'dom', 'alias' => 'dom', 'description' => t('For check plugins that output HTML the dom extension is required') ])); $set->add(new PhpModuleRequirement([ 'condition' => 'curl', 'alias' => 'cURL', 'description' => t( 'To send external commands over Icinga 2\'s API, the cURL module for PHP is required.' ) ])); return $set; } } icingadb-web-1.4.0/library/Icingadb/Setup/RedisPage.php000066400000000000000000000037361516124654200226660ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Setup; use Icinga\Module\Icingadb\Forms\RedisConfigForm; use Icinga\Web\Form; class RedisPage extends Form { public function init() { $this->setName('setup_icingadb_redis'); $this->setTitle(t('Redis')); $this->addDescription(t( 'Please fill out the connection details to access the Redis server.' )); $this->setValidatePartial(true); } public function createElements(array $formData) { $redisConfigForm = new RedisConfigForm(); $redisConfigForm->createElements($formData); if (isset($formData['redis_tls']) && $formData['redis_tls']) { $redisConfigForm->getElement('redis_ca_pem')->setIgnore(false); $redisConfigForm->getElement('redis_cert_pem')->setIgnore(false); $redisConfigForm->getElement('redis_key_pem')->setIgnore(false); } $this->addElements($redisConfigForm->getElements()); $this->addDisplayGroups($redisConfigForm->getDisplayGroups()); } public function isValid($formData) { if (! parent::isValid($formData)) { return false; } if (($el = $this->getElement('skip_validation')) === null || ! $el->isChecked()) { if (! RedisConfigForm::checkRedis($this)) { if ($el === null) { RedisConfigForm::addSkipValidationCheckbox($this); RedisConfigForm::addInsecureCheckboxIfTls($this); } return false; } } return true; } public function isValidPartial(array $formData) { if (! parent::isValidPartial($formData)) { return false; } if (isset($formData['backend_validation'])) { return RedisConfigForm::checkRedis($this); } return true; } } icingadb-web-1.4.0/library/Icingadb/Setup/RedisStep.php000066400000000000000000000161641516124654200227240ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Setup; use Exception; use Icinga\Application\Config; use Icinga\Application\Icinga; use Icinga\Exception\IcingaException; use Icinga\Exception\NotWritableError; use Icinga\File\Storage\LocalFileStorage; use Icinga\Module\Setup\Step; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Table; use ipl\Html\Text; class RedisStep extends Step { /** @var array */ protected $data; /** @var Exception */ protected $error; public function __construct(array $data) { $this->data = $data; } public function apply() { $moduleConfig = [ 'redis' => [ 'tls' => 0 ] ]; $redisConfig = [ 'redis1' => [ 'host' => $this->data['redis1_host'], 'port' => $this->data['redis1_port'] ?: null, 'password' => $this->data['redis1_password'] ?: null ] ]; if (isset($this->data['redis2_host']) && $this->data['redis2_host']) { $redisConfig['redis2'] = [ 'host' => $this->data['redis2_host'], 'port' => $this->data['redis2_port'] ?: null, 'password' => $this->data['redis2_password'] ?: null ]; } if (isset($this->data['redis_tls']) && $this->data['redis_tls']) { $moduleConfig['redis']['tls'] = 1; if (isset($this->data['redis_insecure']) && $this->data['redis_insecure']) { $moduleConfig['redis']['insecure'] = 1; } $storage = new LocalFileStorage(Icinga::app()->getStorageDir( join(DIRECTORY_SEPARATOR, ['modules', 'icingadb', 'redis']) )); foreach (['ca', 'cert', 'key'] as $name) { $textareaName = 'redis_' . $name . '_pem'; if (isset($this->data[$textareaName]) && $this->data[$textareaName]) { $pem = $this->data[$textareaName]; $pemFile = md5($pem) . '-' . $name . '.pem'; if (! $storage->has($pemFile)) { try { $storage->create($pemFile, $pem); } catch (NotWritableError $e) { $this->error = $e; return false; } } $moduleConfig['redis'][$name] = $storage->resolvePath($pemFile); } } } try { $config = Config::module('icingadb', 'config', true); foreach ($moduleConfig as $section => $options) { $config->setSection($section, $options); } $config->saveIni(); $config = Config::module('icingadb', 'redis', true); foreach ($redisConfig as $section => $options) { $config->setSection($section, $options); } $config->saveIni(); } catch (Exception $e) { $this->error = $e; return false; } return true; } public function getSummary() { $topic = new HtmlElement('div', Attributes::create(['class' => 'topic'])); $topic->addHtml(new HtmlElement('p', null, Text::create(mt( 'icingadb', 'Redis will be accessed using the following connection details:' )))); $primaryOptions = new Table(); $primaryOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Host'))), $this->data['redis1_host'] ])); $primaryOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Port'))), $this->data['redis1_port'] ?: 6380 ])); $primaryOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Password'))), $this->data['redis1_password'] ?: mt('icingadb', 'None', 'non-existence of a value') ])); if (isset($this->data['redis2_host']) && $this->data['redis2_host']) { $topic->addHtml( new HtmlElement('h3', null, Text::create(mt('icingadb', 'Primary'))), $primaryOptions ); $secondaryOptions = new Table(); $secondaryOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Host'))), $this->data['redis2_host'] ])); $secondaryOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Port'))), $this->data['redis2_port'] ?: 6380 ])); $secondaryOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create(t('Password'))), $this->data['redis2_password'] ?: mt('icingadb', 'None', 'non-existence of a value') ])); $topic->addHtml( new HtmlElement('h3', null, Text::create(mt('icingadb', 'Secondary'))), $secondaryOptions ); } else { $topic->addHtml($primaryOptions); } $tlsOptions = new Table(); $topic->addHtml($tlsOptions); if (isset($this->data['redis_tls']) && $this->data['redis_tls']) { if (isset($this->data['redis_cert_pem']) && $this->data['redis_cert_pem']) { $tlsOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create('TLS')), Text::create( t('Icinga DB Web will authenticate against Redis with a client' . ' certificate and private key over a secured connection') ) ])); } else { $tlsOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create('TLS')), Text::create(t('Icinga DB Web will use secured Redis connections')) ])); } } else { $tlsOptions->addHtml(Table::row([ new HtmlElement('strong', null, Text::create('TLS')), Text::create(t('No')) ])); } $summary = new HtmlDocument(); $summary->addHtml( new HtmlElement('h2', null, Text::create(mt('icingadb', 'Redis'))), $topic ); return $summary->render(); } public function getReport() { if ($this->error === null) { return [sprintf( mt('icingadb', 'Module configuration update successful: %s'), Config::module('icingab')->getConfigFile() )]; } else { return [ sprintf( mt('icingadb', 'Module configuration update failed: %s'), Config::module('icingab')->getConfigFile() ), sprintf(mt('setup', 'ERROR: %s'), IcingaException::describe($this->error)) ]; } } } icingadb-web-1.4.0/library/Icingadb/Setup/WelcomePage.php000066400000000000000000000030411516124654200232000ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Setup; use Icinga\Web\Form; class WelcomePage extends Form { public function init() { $this->setName('setup_icingadb_welcome'); } public function createElements(array $formData) { $this->addElement( 'note', 'welcome', array( 'value' => t( 'Welcome to the configuration of Icinga DB Web!' ), 'decorators' => array( 'ViewHelper', array('HtmlTag', array('tag' => 'h2')) ) ) ); $this->addElement( 'note', 'description_1', array( 'value' => '

' . t( 'Icinga DB Web is the UI for Icinga DB and provides' . ' a graphical interface to your monitoring environment.' ) . '

', 'decorators' => array('ViewHelper') ) ); $this->addElement( 'note', 'description_2', array( 'value' => '

' . t( 'The wizard will guide you through the configuration to' . ' establish a connection with Icinga DB and Icinga 2.' ) . '

', 'decorators' => array('ViewHelper') ) ); } } icingadb-web-1.4.0/library/Icingadb/Util/000077500000000000000000000000001516124654200201165ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Util/FeatureStatus.php000066400000000000000000000031721516124654200234310ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Util; use ArrayObject; use Icinga\Module\Icingadb\Command\Object\ToggleObjectFeatureCommand; class FeatureStatus extends ArrayObject { public function __construct(string $type, $summary) { $prefix = "{$type}s"; $featureStatus = [ ToggleObjectFeatureCommand::FEATURE_ACTIVE_CHECKS => $this->getFeatureStatus('active_checks_enabled', $prefix, $summary), ToggleObjectFeatureCommand::FEATURE_PASSIVE_CHECKS => $this->getFeatureStatus('passive_checks_enabled', $prefix, $summary), ToggleObjectFeatureCommand::FEATURE_NOTIFICATIONS => $this->getFeatureStatus('notifications_enabled', $prefix, $summary), ToggleObjectFeatureCommand::FEATURE_EVENT_HANDLER => $this->getFeatureStatus('event_handler_enabled', $prefix, $summary), ToggleObjectFeatureCommand::FEATURE_FLAP_DETECTION => $this->getFeatureStatus('flapping_enabled', $prefix, $summary) ]; parent::__construct($featureStatus, ArrayObject::ARRAY_AS_PROPS); } protected function getFeatureStatus(string $feature, string $prefix, $summary): int { $key = "{$prefix}_{$feature}"; $value = (int) $summary->$key; if ($value === 0) { return 0; } $totalKey = "{$prefix}_total"; $total = (int) $summary->$totalKey; if ($value === $total) { return 1; } return 2; } } icingadb-web-1.4.0/library/Icingadb/Util/ObjectSuggestionsCursor.php000066400000000000000000000015321516124654200254670ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Util; use ipl\Sql\Cursor; use Iterator; class ObjectSuggestionsCursor extends Cursor { public function getIterator(): \Traversable { foreach (parent::getIterator() as $key => $value) { // TODO(lippserd): This is a quick and dirty fix for PostgreSQL binary datatypes for which PDO returns // PHP resources that would cause exceptions since resources are not a valid type for attribute values. // We need to do it this way as the suggestion implementation bypasses ORM behaviors here and there. if (is_resource($value)) { $value = stream_get_contents($value); } yield $key => $value; } } } icingadb-web-1.4.0/library/Icingadb/Util/OptimizerHints.php000066400000000000000000000052441516124654200236240ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Util; use Icinga\Application\Config; use ipl\Orm\Query; use ipl\Sql\Adapter\Mysql; use ipl\Sql\Select; use ipl\Stdlib\Str; /** * Helper class to allow disabling the MySQL/MariaDB query optimizer for history queries. * Some versions of these RDBMS perform poorly with history queries, * particularly when the optimizer changes join order or uses block nested loop joins. * Ideally, testing across all RDBMS versions to identify when the optimizer fails and adjusting queries or * using optimizer switches would be preferable, but this level of effort is not justified at the moment. */ readonly class OptimizerHints { public const DISABLE_OPTIMIZER_HINT = '/*+ NO_BNL() */ STRAIGHT_JOIN'; /** * If optimizer disabling is enabled for history queries, * injects an optimizer hint into SELECT queries for a MySQL/MariaDB Query object, * forcing the RDBMS to disable the optimizer * * @param Query $q * * @return void */ public static function disableOptimizerForHistoryQueries(Query $q): void { if (static::shouldDisableOptimizerForHistoryQueries() && $q->getDb()->getAdapter() instanceof Mysql) { // Locates the first string column, prepends the optimizer hint, // and resets columns to ensure it appears first in the SELECT statement: $q->on(Query::ON_SELECT_ASSEMBLED, static function (Select $select) { $columns = $select->getColumns(); foreach ($columns as $alias => $column) { if (is_string($column)) { if (Str::startsWith($column, static::DISABLE_OPTIMIZER_HINT)) { return; } unset($columns[$alias]); if (is_int($alias)) { array_unshift($columns, static::DISABLE_OPTIMIZER_HINT . " $column"); } else { $columns = [$alias => static::DISABLE_OPTIMIZER_HINT . " $column"] + $columns; } $select->resetColumns()->columns($columns); return; } } }); } } /** * Determines whether to disable the query optimizer for history queries. * * @return bool */ protected static function shouldDisableOptimizerForHistoryQueries(): bool { return Config::module('icingadb')->get('icingadb', 'disable_optimizer_for_history_queries', false); } } icingadb-web-1.4.0/library/Icingadb/Util/PerfData.php000066400000000000000000000427101516124654200223210ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Util; use Icinga\Module\Icingadb\Common\ServiceStates; use Icinga\Web\Widget\Chart\InlinePie; use InvalidArgumentException; use LogicException; class PerfData { public const PERFDATA_OK = 'ok'; public const PERFDATA_WARNING = 'warning'; public const PERFDATA_CRITICAL = 'critical'; /** * The performance data value being parsed * * @var string */ protected $perfdataValue; /** * Unit of measurement (UOM) * * @var string */ protected $unit; /** * The label * * @var string */ protected $label; /** * The value * * @var ?float */ protected $value; /** * The minimum value * * @var ?float */ protected $minValue; /** * The maximum value * * @var ?float */ protected $maxValue; /** * The WARNING threshold * * @var ThresholdRange */ protected $warningThreshold; /** * The CRITICAL threshold * * @var ThresholdRange */ protected $criticalThreshold; /** * The raw value * * @var ?string */ protected $rawValue; /** * The raw minimum value * * @var ?string */ protected $rawMinValue; /** * The raw maximum value * * @var ?string */ protected $rawMaxValue; /** * Create a new PerfData object based on the given performance data label and value * * @param string $label The perfdata label * @param string $value The perfdata value */ public function __construct(string $label, string $value) { $this->perfdataValue = $value; $this->label = $label; $this->parse(); if ($this->unit === '%') { if ($this->minValue === null) { $this->minValue = 0.0; } if ($this->maxValue === null) { $this->maxValue = 100.0; } } $warn = $this->warningThreshold->getMax(); if ($warn !== null) { $crit = $this->criticalThreshold->getMax(); if ($crit !== null && $warn > $crit) { $this->warningThreshold->setInverted(); $this->criticalThreshold->setInverted(); } } } /** * Return a new PerfData object based on the given performance data key=value pair * * @param string $perfdata The key=value pair to parse * * @return PerfData * * @throws InvalidArgumentException In case the given performance data has no content or a invalid format */ public static function fromString(string $perfdata): self { if (empty($perfdata)) { throw new InvalidArgumentException('PerfData::fromString expects a string with content'); } elseif (strpos($perfdata, '=') === false) { throw new InvalidArgumentException( 'PerfData::fromString expects a key=value formatted string. Got "' . $perfdata . '" instead' ); } list($label, $value) = explode('=', $perfdata, 2); return new static(trim($label), trim($value)); } /** * Return whether this performance data's value is a number * * @return bool True in case it's a number, otherwise False */ public function isNumber(): bool { return $this->unit === null; } /** * Return whether this performance data's value are seconds * * @return bool True in case it's seconds, otherwise False */ public function isSeconds(): bool { return $this->unit === 's'; } /** * Return whether this performance data's value is a temperature * * @return bool True in case it's temperature, otherwise False */ public function isTemperature(): bool { return in_array($this->unit, array('C', 'F', 'K')); } /** * Return whether this performance data's value is in percentage * * @return bool True in case it's in percentage, otherwise False */ public function isPercentage(): bool { return $this->unit === '%'; } /** * Get whether this perf data's value is in packets * * @return bool True in case it's in packets */ public function isPackets(): bool { return $this->unit === 'packets'; } /** * Get whether this perf data's value is in lumen * * @return bool */ public function isLumens(): bool { return $this->unit === 'lm'; } /** * Get whether this perf data's value is in decibel-milliwatts * * @return bool */ public function isDecibelMilliWatts(): bool { return $this->unit === 'dBm'; } /** * Get whether this data's value is in bits * * @return bool */ public function isBits(): bool { return $this->unit === 'b'; } /** * Return whether this performance data's value is in bytes * * @return bool True in case it's in bytes, otherwise False */ public function isBytes(): bool { return $this->unit === 'B'; } /** * Get whether this data's value is in watt hours * * @return bool */ public function isWattHours(): bool { return $this->unit === 'Wh'; } /** * Get whether this data's value is in watt * * @return bool */ public function isWatts(): bool { return $this->unit === 'W'; } /** * Get whether this data's value is in ampere * * @return bool */ public function isAmperes(): bool { return $this->unit === 'A'; } /** * Get whether this data's value is in ampere seconds * * @return bool */ public function isAmpSeconds(): bool { return $this->unit === 'As'; } /** * Get whether this data's value is in volts * * @return bool */ public function isVolts(): bool { return $this->unit === 'V'; } /** * Get whether this data's value is in ohm * * @return bool */ public function isOhms(): bool { return $this->unit === 'O'; } /** * Get whether this data's value is in grams * * @return bool */ public function isGrams(): bool { return $this->unit === 'g'; } /** * Get whether this data's value is in Litters * * @return bool */ public function isLiters(): bool { return $this->unit === 'l'; } /** * Return whether this performance data's value is a counter * * @return bool True in case it's a counter, otherwise False */ public function isCounter(): bool { return $this->unit === 'c'; } /** * Returns whether it is possible to display a visual representation * * @return bool True when the perfdata is visualizable */ public function isVisualizable(): bool { return isset($this->minValue, $this->maxValue, $this->value) && $this->isValid(); } /** * Return this perfomance data's label */ public function getLabel(): string { return $this->label; } /** * Return the value or null if it is unknown (U) * * @return null|float */ public function getValue() { return $this->value; } /** * Return the unit as a string * * @return ?string */ public function getUnit() { return $this->unit; } /** * Return the value as percentage (0-100) * * @return null|float */ public function getPercentage() { if ($this->isPercentage()) { return $this->value; } if ($this->maxValue !== null) { $minValue = $this->minValue !== null ? $this->minValue : 0.0; if ($this->maxValue == $minValue) { return null; } if ($this->value > $minValue) { return (($this->value - $minValue) / ($this->maxValue - $minValue)) * 100; } } } /** * Return this performance data's warning treshold * * @return ThresholdRange */ public function getWarningThreshold(): ThresholdRange { return $this->warningThreshold; } /** * Return this performance data's critical treshold * * @return ThresholdRange */ public function getCriticalThreshold(): ThresholdRange { return $this->criticalThreshold; } /** * Return the minimum value or null if it is not available * * @return ?float */ public function getMinimumValue() { return $this->minValue; } /** * Return the maximum value or null if it is not available * * @return null|float */ public function getMaximumValue() { return $this->maxValue; } /** * Return this performance data as string * * @return string */ public function __toString() { return $this->formatLabel(); } /** * Parse the current performance data value * * @todo Handle optional min/max if UOM == % */ protected function parse() { $parts = explode(';', $this->perfdataValue); $matches = array(); if (preg_match('@^(U|-?(?:\d+)?(?:\.\d+)?)([a-zA-TV-Z%°]{1,3})$@u', $parts[0], $matches)) { $this->unit = $matches[2]; $value = $matches[1]; } else { $value = $parts[0]; } if (! is_numeric($value)) { if ($value !== 'U') { $this->rawValue = $parts[0]; } $this->value = null; } else { $this->value = floatval($value); } switch (count($parts)) { /* @noinspection PhpMissingBreakStatementInspection */ case 5: if ($parts[4] !== '') { if (is_numeric($parts[4])) { $this->maxValue = floatval($parts[4]); } else { $this->rawMaxValue = $parts[4]; } } /* @noinspection PhpMissingBreakStatementInspection */ case 4: if ($parts[3] !== '') { if (is_numeric($parts[3])) { $this->minValue = floatval($parts[3]); } else { $this->rawMinValue = $parts[3]; } } /* @noinspection PhpMissingBreakStatementInspection */ case 3: $this->criticalThreshold = ThresholdRange::fromString(trim($parts[2])); // Fallthrough case 2: $this->warningThreshold = ThresholdRange::fromString(trim($parts[1])); } if ($this->warningThreshold === null) { $this->warningThreshold = new ThresholdRange(); } if ($this->criticalThreshold === null) { $this->criticalThreshold = new ThresholdRange(); } } protected function calculatePieChartData(): array { $rawValue = $this->getValue(); $minValue = $this->getMinimumValue() !== null ? $this->getMinimumValue() : 0; $usedValue = ($rawValue - $minValue); $green = $orange = $red = 0; if ($this->criticalThreshold->contains($rawValue)) { if ($this->warningThreshold->contains($rawValue)) { $green = $usedValue; } else { $orange = $usedValue; } } else { $red = $usedValue; } return array($green, $orange, $red, ($this->getMaximumValue() - $minValue) - $usedValue); } public function asInlinePie(): InlinePie { if (! $this->isVisualizable()) { throw new LogicException('Cannot calculate piechart data for unvisualizable perfdata entry.'); } $data = $this->calculatePieChartData(); $pieChart = new InlinePie($data, $this); $pieChart->setColors(array('#44bb77', '#ffaa44', '#ff5566', '#ddccdd')); return $pieChart; } /** * Format the given value depending on the currently used unit */ protected function format($value) { if ($value === null) { return null; } if ($value instanceof ThresholdRange) { if (! $value->isValid()) { return (string) $value; } if ($value->getMin()) { return (string) $value; } $max = $value->getMax(); return $max === null ? '' : $this->format($max); } switch (true) { case $this->isPercentage(): return (string) $value . '%'; case $this->isPackets(): return (string) $value . 'packets'; case $this->isLumens(): return (string) $value . 'lm'; case $this->isDecibelMilliWatts(): return (string) $value . 'dBm'; case $this->isCounter(): return (string) $value . 'c'; case $this->isTemperature(): return (string) $value . $this->unit; case $this->isBits(): return PerfDataFormat::bits($value); case $this->isBytes(): return PerfDataFormat::bytes($value); case $this->isSeconds(): return PerfDataFormat::seconds($value); case $this->isWatts(): return PerfDataFormat::watts($value); case $this->isWattHours(): return PerfDataFormat::wattHours($value); case $this->isAmperes(): return PerfDataFormat::amperes($value); case $this->isAmpSeconds(): return PerfDataFormat::ampereSeconds($value); case $this->isVolts(): return PerfDataFormat::volts($value); case $this->isOhms(): return PerfDataFormat::ohms($value); case $this->isGrams(): return PerfDataFormat::grams($value); case $this->isLiters(): return PerfDataFormat::liters($value); case ! is_numeric($value): return $value; default: return number_format($value, 2) . ($this->unit !== null ? ' ' . $this->unit : ''); } } /** * Format the title string that represents this perfdata set * * @param bool $html * * @return string */ public function formatLabel(bool $html = false): string { return sprintf( $html ? '%s %s (%s%%)' : '%s %s (%s%%)', htmlspecialchars($this->getLabel()), $this->format($this->value), number_format($this->getPercentage() ?? 0, 2) ); } public function toArray(): array { return [ 'label' => $this->getLabel(), 'value' => isset($this->value) ? $this->format($this->value) : $this->rawValue, 'min' => (string) ( ! $this->isPercentage() ? (isset($this->minValue) ? $this->format($this->minValue) : $this->rawMinValue) : null ), 'max' => (string) ( ! $this->isPercentage() ? (isset($this->maxValue) ? $this->format($this->maxValue) : $this->rawMaxValue) : null ), 'warn' => $this->format($this->warningThreshold), 'crit' => $this->format($this->criticalThreshold) ]; } /** * Return the state indicated by this perfdata * * @return int */ public function getState(): int { if (! is_numeric($this->value)) { return ServiceStates::UNKNOWN; } if (! $this->criticalThreshold->contains($this->value)) { return ServiceStates::CRITICAL; } if (! $this->warningThreshold->contains($this->value)) { return ServiceStates::WARNING; } return ServiceStates::OK; } /** * Return whether the state indicated by this perfdata is worse than * the state indicated by the other perfdata * CRITICAL > UNKNOWN > WARNING > OK * * @param PerfData $rhs the other perfdata * * @return bool */ public function worseThan(PerfData $rhs): bool { if (($state = $this->getState()) === ($rhsState = $rhs->getState())) { return $this->getPercentage() > $rhs->getPercentage(); } if ($state === ServiceStates::CRITICAL) { return true; } if ($state === ServiceStates::UNKNOWN) { return $rhsState !== ServiceStates::CRITICAL; } if ($state === ServiceStates::WARNING) { return $rhsState === ServiceStates::OK; } return false; } /** * Returns whether the performance data can be evaluated * * @return bool */ public function isValid(): bool { return ! isset($this->rawValue) && ! isset($this->rawMinValue) && ! isset($this->rawMaxValue) && $this->criticalThreshold->isValid() && $this->warningThreshold->isValid(); } } icingadb-web-1.4.0/library/Icingadb/Util/PerfDataFormat.php000066400000000000000000000107531516124654200234740ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Util; class PerfDataFormat { protected static $instance; protected static $generalBase = 1000; protected static $bitPrefix = ['b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']; protected static $bytePrefix = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; protected static $wattHourPrefix = ['Wh', 'kWh', 'MWh', 'GWh', 'TWh', 'PWh', 'EWh', 'ZWh', 'YWh']; protected static $wattPrefix = [-1 => 'mW', 'W', 'kW', 'MW', 'GW']; protected static $amperePrefix = [-3 => 'nA', -2 => 'µA', -1 => 'mA', 'A', 'kA', 'MA', 'GA']; protected static $ampSecondPrefix = [-2 => 'µAs', -1 => 'mAs', 'As', 'kAs', 'MAs', 'GAs']; protected static $voltPrefix = [-2 => 'µV', -1 => 'mV', 'V', 'kV', 'MV', 'GV']; protected static $ohmPrefix = ['Ω']; protected static $gramPrefix = [ -5 => 'fg', -4 => 'pg', -3 => 'ng', -2 => 'µg', -1 => 'mg', 'g', 'kg', 't', 'ktǂ', 'Mt', 'Gt' ]; protected static $literPrefix = [ -5 => 'fl', -4 => 'pl', -3 => 'nl', -2 => 'µl', -1 => 'ml', 'l', 'kl', 'Ml', 'Gl', 'Tl', 'Pl' ]; protected static $secondPrefix = [-3 => 'ns', -2 => 'µs', -1 => 'ms', 's']; public static function getInstance(): self { if (self::$instance === null) { self::$instance = new PerfDataFormat(); } return self::$instance; } public static function bits($value): string { return self::formatForUnits($value, self::$bitPrefix, self::$generalBase); } public static function bytes($value): string { return self::formatForUnits($value, self::$bytePrefix, self::$generalBase); } public static function wattHours($value): string { return self::formatForUnits($value, self::$wattHourPrefix, self::$generalBase); } public static function watts($value): string { return self::formatForUnits($value, self::$wattPrefix, self::$generalBase); } public static function amperes($value): string { return self::formatForUnits($value, self::$amperePrefix, self::$generalBase); } public static function ampereSeconds($value): string { return self::formatForUnits($value, self::$ampSecondPrefix, self::$generalBase); } public static function volts($value): string { return self::formatForUnits($value, self::$voltPrefix, self::$generalBase); } public static function ohms($value): string { return self::formatForUnits($value, self::$ohmPrefix, self::$generalBase); } public static function grams($value): string { return self::formatForUnits($value, self::$gramPrefix, self::$generalBase); } public static function liters($value): string { return self::formatForUnits($value, self::$literPrefix, self::$generalBase); } public static function seconds($value): string { $value = (float) $value; $absValue = abs($value); if ($absValue < 60) { return self::formatForUnits($value, self::$secondPrefix, self::$generalBase); } elseif ($absValue < 3600) { return sprintf('%0.2f m', $value / 60); } elseif ($absValue < 86400) { return sprintf('%0.2f h', $value / 3600); } return sprintf('%0.2f d', $value / 86400); } protected static function formatForUnits(float $value, array &$units, int $base): string { $sign = ''; if ($value < 0) { $value = abs($value); $sign = '-'; } if ($value == 0) { $pow = $result = 0; } else { $pow = floor(log($value, $base)); // Identify nearest unit if unknown while (! isset($units[$pow])) { if ($pow < 0) { $pow++; } else { $pow--; } } $result = $value / pow($base, $pow); } // 1034.23 looks better than 1.03, but 2.03 is fine: if ($pow > 0 && $result < 2) { $result = $value / pow($base, --$pow); } return sprintf( '%s%0.2f %s', $sign, $result, $units[$pow] ); } } icingadb-web-1.4.0/library/Icingadb/Util/PerfDataSet.php000066400000000000000000000111141516124654200227670ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Util; use ArrayIterator; use IteratorAggregate; class PerfDataSet implements IteratorAggregate { /** * The performance data being parsed * * @var string */ protected $perfdataStr; /** * The current parsing position * * @var int */ protected $parserPos = 0; /** * A list of PerfData objects * * @var array */ protected $perfdata = array(); /** * Create a new set of performance data * * @param string $perfdataStr A space separated list of label/value pairs */ protected function __construct(string $perfdataStr) { if (($perfdataStr = trim($perfdataStr)) !== '') { $this->perfdataStr = $perfdataStr; $this->parse(); } } /** * Return a iterator for this set of performance data * * @return ArrayIterator */ public function getIterator(): ArrayIterator { return new ArrayIterator($this->asArray()); } /** * Return a new set of performance data * * @param string $perfdataStr A space separated list of label/value pairs * * @return PerfDataSet */ public static function fromString(string $perfdataStr): self { return new static($perfdataStr); } /** * Return this set of performance data as array * * @return array */ public function asArray(): array { return $this->perfdata; } /** * Parse the current performance data */ protected function parse() { while ($this->parserPos < strlen($this->perfdataStr)) { $label = trim($this->readLabel()); $value = trim($this->readUntil(' ')); if ($label) { $this->perfdata[] = new PerfData($label, $value); } } uasort( $this->perfdata, function ($a, $b) { if ($a->isVisualizable() && ! $b->isVisualizable()) { return -1; } elseif (! $a->isVisualizable() && $b->isVisualizable()) { return 1; } elseif (! $a->isVisualizable() && ! $b->isVisualizable()) { return 0; } return $a->worseThan($b) ? -1 : ($b->worseThan($a) ? 1 : 0); } ); } /** * Return the next label found in the performance data * * @return string The label found */ protected function readLabel(): string { $this->skipSpaces(); if (in_array($this->perfdataStr[$this->parserPos], array('"', "'"))) { $quoteChar = $this->perfdataStr[$this->parserPos++]; $label = $this->readUntil($quoteChar, '='); $this->parserPos++; if ($this->perfdataStr[$this->parserPos] === '=') { $this->parserPos++; } } else { $label = $this->readUntil('='); $this->parserPos++; } $this->skipSpaces(); return $label; } /** * Return all characters between the current parser position and the given character * * @param string $stopChar The character on which to stop * @param ?string $backtrackOn The character on which to backtrack * * @return string */ protected function readUntil(string $stopChar, ?string $backtrackOn = null): string { $start = $this->parserPos; $breakCharEncounteredAt = null; $stringExhaustedAt = strlen($this->perfdataStr); while ($this->parserPos < $stringExhaustedAt) { if ($this->perfdataStr[$this->parserPos] === $stopChar) { break; } elseif ($breakCharEncounteredAt === null && $this->perfdataStr[$this->parserPos] === $backtrackOn) { $breakCharEncounteredAt = $this->parserPos; } $this->parserPos++; } if ($breakCharEncounteredAt !== null && $this->parserPos === $stringExhaustedAt) { $this->parserPos = $breakCharEncounteredAt; } return substr($this->perfdataStr, $start, $this->parserPos - $start); } /** * Advance the parser position to the next non-whitespace character */ protected function skipSpaces() { while ($this->parserPos < strlen($this->perfdataStr) && $this->perfdataStr[$this->parserPos] === ' ') { $this->parserPos++; } } } icingadb-web-1.4.0/library/Icingadb/Util/PluginOutput.php000066400000000000000000000132241516124654200233100ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Util; use Icinga\Module\Icingadb\Hook\PluginOutputHook; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Web\Helper\HtmlPurifier; use InvalidArgumentException; use ipl\Html\HtmlString; use ipl\Orm\Model; use LogicException; class PluginOutput extends HtmlString { /** @var string[] Patterns to be replaced in plain text plugin output */ protected const TEXT_PATTERNS = [ '~\\\t~', '~\\\n~', '~(\[|\()OK(\]|\))~', '~(\[|\()WARNING(\]|\))~', '~(\[|\()CRITICAL(\]|\))~', '~(\[|\()UNKNOWN(\]|\))~', '~(\[|\()UP(\]|\))~', '~(\[|\()DOWN(\]|\))~', '~(\[|\()INFO(\]|\))~', '~\@{6,}~' ]; /** @var string[] Replacements for {@see PluginOutput::TEXT_PATTERNS} */ protected const TEXT_REPLACEMENTS = [ "\t", "\n", '', '', '', '', '', '', '', '@@@@@@' ]; /** @var string[] Patterns to be replaced in html plugin output */ protected const HTML_PATTERNS = [ '~\\\t~', '~\\\n~' ]; /** @var string[] Replacements for {@see PluginOutput::HTML_PATTERNS} */ protected const HTML_REPLACEMENTS = [ "\t", "\n" ]; /** @var string Already rendered output */ protected $renderedOutput; /** @var bool Whether the output contains HTML */ protected $isHtml; /** @var int The maximum amount of characters to process */ protected $characterLimit = 1000; /** @var bool Whether output will be enriched */ protected $enrichOutput = true; /** @var string The name of the command that produced the output */ protected $commandName; /** * Get whether the output contains HTML * * Requires the output being already rendered. * * @return bool * * @throws LogicException In case the output hasn't been rendered yet */ public function isHtml(): bool { if ($this->isHtml === null) { if (empty($this->getContent())) { // "Nothing" can't be HTML return false; } throw new LogicException('Output not rendered yet'); } return $this->isHtml; } /** * Set the maximum amount of characters to process * * @param int $limit * * @return $this */ public function setCharacterLimit(int $limit): self { $this->characterLimit = $limit; return $this; } /** * Set whether the output should be enriched * * @param bool $state * * @return $this */ public function setEnrichOutput(bool $state = true): self { $this->enrichOutput = $state; return $this; } /** * Set name of the command that produced the output * * @param string $name * * @return $this */ public function setCommandName(string $name): self { $this->commandName = $name; return $this; } /** * Render plugin output of the given object * * @param Host|Service $object * * @return static * * @throws InvalidArgumentException If $object is neither a host nor a service */ public static function fromObject(Model $object): self { if (! $object instanceof Host && ! $object instanceof Service) { throw new InvalidArgumentException( sprintf('Object is not a host or service, got %s instead', get_class($object)) ); } return (new static($object->state->output . "\n" . $object->state->long_output)) ->setCommandName($object->checkcommand_name); } public function render(): string { if ($this->renderedOutput !== null) { return $this->renderedOutput; } $output = parent::render(); if (empty($output)) { return ''; } if ($this->commandName !== null) { $output = PluginOutputHook::processOutput($output, $this->commandName, $this->enrichOutput); } $output = substr($output, 0, $this->characterLimit); $this->isHtml = (bool) preg_match('~<\w+(?>\s\w+=[^>]*)?>~', $output); if ($this->isHtml) { if ($this->enrichOutput) { $output = preg_replace(self::TEXT_PATTERNS, self::TEXT_REPLACEMENTS, $output); } else { $output = preg_replace(self::HTML_PATTERNS, self::HTML_REPLACEMENTS, $output); } $output = HtmlPurifier::process($output); } else { $output = preg_replace( self::TEXT_PATTERNS, self::TEXT_REPLACEMENTS, htmlspecialchars($output, ENT_COMPAT | ENT_SUBSTITUTE | ENT_HTML5, null, false) ); } $output = trim($output); // Add zero-width space after commas which are not followed by a whitespace character // in oder to help browsers to break words in plugin output $output = preg_replace('/,(?=[^\s])/', ',​', $output); $this->renderedOutput = $output; return $output; } } icingadb-web-1.4.0/library/Icingadb/Util/ThresholdRange.php000066400000000000000000000111011516124654200235320ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Util; /** * The warning/critical threshold of a measured value */ class ThresholdRange { /** * The smallest value inside the range (null stands for -∞) * * @var float|null */ protected $min; /** * The biggest value inside the range (null stands for ∞) * * @var float|null */ protected $max; /** * Whether to invert the result of contains() * * @var bool */ protected $inverted = false; /** * The unmodified range as passed to fromString() * * @var string */ protected $raw; /** * Whether the threshold range is valid * * @var bool */ protected $isValid = true; /** * Create a new instance based on a threshold range conforming to * * @param string $rawRange * * @return ThresholdRange */ public static function fromString(string $rawRange): self { $range = new static(); $range->raw = $rawRange; if ($rawRange == '') { return $range; } $rawRange = ltrim($rawRange); if (substr($rawRange, 0, 1) === '@') { $range->setInverted(); $rawRange = substr($rawRange, 1); } if (strpos($rawRange, ':') === false) { $min = 0.0; $max = trim($rawRange); if (! is_numeric($max)) { $range->isValid = false; return $range; } $max = floatval(trim($rawRange)); } else { list($min, $max) = explode(':', $rawRange, 2); $min = trim($min); $max = trim($max); switch ($min) { case '': $min = 0.0; break; case '~': $min = null; break; default: if (! is_numeric($min)) { $range->isValid = false; return $range; } $min = floatval($min); } if (! empty($max) && ! is_numeric($max)) { $range->isValid = false; return $range; } $max = empty($max) ? null : floatval($max); } return $range->setMin($min) ->setMax($max); } /** * Set the smallest value inside the range (null stands for -∞) * * @param float|null $min * * @return $this */ public function setMin(?float $min): self { $this->min = $min; return $this; } /** * Get the smallest value inside the range (null stands for -∞) * * @return float|null */ public function getMin() { return $this->min; } /** * Set the biggest value inside the range (null stands for ∞) * * @param float|null $max * * @return $this */ public function setMax(?float $max): self { $this->max = $max; return $this; } /** * Get the biggest value inside the range (null stands for ∞) * * @return float|null */ public function getMax() { return $this->max; } /** * Set whether to invert the result of contains() * * @param bool $inverted * * @return $this */ public function setInverted(bool $inverted = true): self { $this->inverted = $inverted; return $this; } /** * Get whether to invert the result of contains() * * @return bool */ public function isInverted(): bool { return $this->inverted; } /** * Return whether $value is inside $this * * @param float $value * * @return bool */ public function contains(float $value): bool { return (bool) ($this->inverted ^ ( ($this->min === null || $this->min <= $value) && ($this->max === null || $this->max >= $value) )); } /** * Return whether the threshold range is valid * * @return bool */ public function isValid() { return $this->isValid; } /** * Return the textual representation of $this, suitable for fromString() * * @return string */ public function __toString() { return (string) $this->raw; } } icingadb-web-1.4.0/library/Icingadb/View/000077500000000000000000000000001516124654200201135ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/View/BaseHostAndServiceRenderer.php000066400000000000000000000334671516124654200260040ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Common\HostStates; use Icinga\Module\Icingadb\Common\Icons; use Icinga\Module\Icingadb\Common\ServiceStates; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Util\PerfDataSet; use Icinga\Module\Icingadb\Util\PluginOutput; use Icinga\Module\Icingadb\Widget\CheckAttempt; use Icinga\Module\Icingadb\Widget\IconImage; use Icinga\Module\Icingadb\Widget\PluginOutputContainer; use Icinga\Module\Icingadb\Widget\StateChange; use ipl\Html\Attributes; use ipl\Html\Html; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\HtmlString; use ipl\Html\Text; use ipl\Html\ValidHtml; use ipl\I18n\Translation; use ipl\Web\Common\ItemRenderer; use ipl\Web\Layout\ItemLayout; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\Icon; use ipl\Web\Widget\StateBall; use ipl\Web\Widget\TimeSince; /** * @template Item of Host|Service * * @implements ItemRenderer */ abstract class BaseHostAndServiceRenderer implements ItemRenderer { use Translation; /** * Create subject for the given item * * @param Item $item The item to create subject for * * @param string $layout The name of the layout * * @return ValidHtml */ abstract protected function createSubject($item, string $layout): ValidHtml; public function assembleAttributes($item, Attributes $attributes, string $layout): void { if ($item->state->is_overdue) { $attributes->get('class')->addValue('overdue'); } } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { if ($layout === 'header') { if ($item->state->state_type === 'soft') { $stateType = 'soft_state'; $previousStateType = 'previous_soft_state'; if ($item->state->previous_soft_state === 0) { $previousStateType = 'hard_state'; } } else { $stateType = 'hard_state'; $previousStateType = 'previous_hard_state'; if ($item->state->hard_state === $item->state->previous_hard_state) { $previousStateType = 'previous_soft_state'; } } if ($item instanceof Host) { $state = HostStates::text($item->state->$stateType); $previousState = HostStates::text($item->state->$previousStateType); } else { $state = ServiceStates::text($item->state->$stateType); $previousState = ServiceStates::text($item->state->$previousStateType); } $stateChange = new StateChange($state, $previousState); if ($stateType === 'soft_state') { $stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE); } if ($previousStateType === 'previous_soft_state') { $stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE); if ($stateType === 'soft_state') { $visual->getAttributes()->add('class', 'small-state-change'); } } $stateChange->setIcon($this->getStateBallIcon($item)); $stateChange->setHandled( $item->state->is_problem && ($item->state->is_handled || ! $item->state->is_reachable) ); $visual->addHtml($stateChange); return; } $ballSize = $layout === 'minimal' ? StateBall::SIZE_BIG : StateBall::SIZE_LARGE; $stateBall = new StateBall($item->state->getStateText(), $ballSize); $stateBall->add($this->getStateBallIcon($item)); if ($item->state->is_problem && ($item->state->is_handled || ! $item->state->is_reachable)) { $stateBall->getAttributes()->add('class', 'handled'); } $visual->addHtml($stateBall); if ($layout !== 'minimal' && $item->state->state_type === 'soft') { $visual->addHtml( new CheckAttempt((int) $item->state->check_attempt, (int) $item->max_check_attempts) ); } } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { if ($item->state->soft_state === null && $item->state->output === null) { $caption->addHtml(Text::create($this->translate('Waiting for Icinga DB to synchronize the state.'))); } else { if (empty($item->state->output)) { $pluginOutput = new EmptyState($this->translate('Output unavailable.')); } else { $pluginOutput = new PluginOutputContainer(PluginOutput::fromObject($item)); } $caption->addHtml($pluginOutput); } } public function assembleTitle($item, HtmlDocument $title, string $layout): void { $title->addHtml(Html::sprintf( $this->translate('%s is %s', ' is '), $this->createSubject($item, $layout), Html::tag('span', ['class' => 'state-text'], $item->state->getStateTextTranslated()) )); if (isset($item->state->affects_children) && $item->state->affects_children) { $total = (int) $item->total_children; $title->addHtml(new HtmlElement( 'span', Attributes::create([ 'class' => 'affected-objects', 'title' => sprintf( $this->translatePlural( '%d affected object', 'Up to %d affected objects', $total ), $total ) ]), new Icon(Icons::UNREACHABLE), Text::create($total) )); } } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { if ($item->state->is_overdue) { $since = new TimeSince($item->state->next_update->getTimestamp()); $since->prepend($this->translate('Overdue') . ' '); $since->prependHtml(new Icon(Icons::WARNING)); $info->addHtml($since); } elseif ($item->state->last_state_change !== null && $item->state->last_state_change->getTimestamp() > 0) { $info->addHtml(new TimeSince($item->state->last_state_change->getTimestamp())); } } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { $pieChartLimit = 5; $statusIcons = new HtmlElement('div', Attributes::create(['class' => 'status-icons'])); $isService = $item instanceof Service; if ( ($isService && $item->state->last_comment->service_id === $item->id) || $item->state->last_comment->host_id === $item->id ) { $comment = $item->state->last_comment; if ($isService) { $comment->service = $item; } else { $comment->host = $item; } $commentItem = new ItemLayout( $comment, (new CommentRenderer()) ->setTicketLinkDisabled() ->setNoObjectLink() ->setNoSubjectLink() ); $statusIcons->addHtml( new HtmlElement( 'div', Attributes::create(['class' => 'comment-wrapper']), new HtmlElement( 'div', $commentItem->getAttributes()->add('class', 'comment-popup'), $commentItem ), (new Icon('comments', ['class' => 'comment-icon'])) ) ); } if ($item->state->is_acknowledged) { $title = $isService ? sprintf( $this->translate('Service "%s" on "%s" is acknowledged'), $item->display_name, $item->host->display_name ) : sprintf($this->translate('Host "%s" is acknowledged'), $item->display_name); $statusIcons->addHtml(new Icon(Icons::IS_ACKNOWLEDGED, ['title' => $title])); } if ($item->state->in_downtime) { if ($isService) { $message = $item->state->is_handled ? $this->translate('Service "%s" on "%s" is handled by Downtime') : $this->translate('Service "%s" on "%s" is in Downtime'); $title = sprintf($message, $item->display_name, $item->host->display_name); } else { $message = $item->state->is_handled ? $this->translate('Host "%s" is handled by Downtime') : $this->translate('Host "%s" is in Downtime'); $title = sprintf($message, $item->display_name); } $statusIcons->addHtml(new Icon(Icons::IN_DOWNTIME, ['title' => $title])); } if ($item->state->is_flapping) { $title = $isService ? sprintf( $this->translate('Service "%s" on "%s" is in flapping state'), $item->display_name, $item->host->display_name ) : sprintf($this->translate('Host "%s" is in flapping state'), $item->display_name); $statusIcons->addHtml(new Icon(Icons::IS_FLAPPING, ['title' => $title])); } if (! $item->state->is_reachable) { $title = $isService ? sprintf( $this->translate('Service "%s" on "%s" is unreachable'), $item->display_name, $item->host->display_name ) : sprintf($this->translate('Host "%s" is unreachable'), $item->display_name); $statusIcons->addHtml(new Icon(Icons::HOST_DOWN, ['title' => $title])); } if (! $item->notifications_enabled) { $title = $isService ? sprintf( $this->translate('Service "%s" on "%s" has notifications disabled'), $item->display_name, $item->host->display_name ) : sprintf($this->translate('Host "%s" has notifications disabled'), $item->display_name); $statusIcons->addHtml(new Icon(Icons::NOTIFICATIONS_DISABLED, ['title' => $title])); } if (! $item->active_checks_enabled) { $title = $isService ? sprintf( $this->translate('Service "%s" on "%s" has active checks disabled'), $item->display_name, $item->host->display_name ) : sprintf($this->translate('Host "%s" has active checks disabled'), $item->display_name); $statusIcons->addHtml(new Icon(Icons::ACTIVE_CHECKS_DISABLED, ['title' => $title])); } $performanceData = new HtmlElement('div', Attributes::create(['class' => 'performance-data'])); if ($item->state->performance_data) { $pieChartData = PerfDataSet::fromString($item->state->normalized_performance_data)->asArray(); $pies = []; foreach ($pieChartData as $i => $perfdata) { if ($perfdata->isVisualizable()) { $pies[] = $perfdata->asInlinePie()->render(); } // Check if number of visualizable pie charts is larger than $PIE_CHART_LIMIT if (count($pies) > $pieChartLimit) { break; } } $maxVisiblePies = $pieChartLimit - 2; $numOfPies = count($pies); foreach ($pies as $i => $pie) { if ( // Show max. 5 elements: if there are more than 5, show 4 + `…` $i > $maxVisiblePies && $numOfPies > $pieChartLimit ) { $performanceData->addHtml(new HtmlElement('span', null, Text::create('…'))); break; } $performanceData->addHtml(HtmlString::create($pie)); } } if (! $statusIcons->isEmpty()) { $footer->addHtml($statusIcons); } if (! $performanceData->isEmpty()) { $footer->addHtml($performanceData); } } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { if ($name === 'icon-image') { if (isset($item->icon_image->icon_image)) { $element->addHtml(new IconImage($item->icon_image->icon_image, $item->icon_image_alt)); } return true; } return false; } protected function getStateBallIcon(Host|Service $item): ?Icon { $icon = $item->state->getIcon(); if ($icon === null) { if (! $item->notifications_enabled) { $icon = new Icon(Icons::NOTIFICATIONS_DISABLED, [ 'title' => sprintf( '%s (%s)', strtoupper($item->state->getStateTextTranslated()), $this->translate('has notifications disabled') ) ]); } elseif (! $item->active_checks_enabled) { $icon = new Icon(Icons::ACTIVE_CHECKS_DISABLED, [ 'title' => sprintf( '%s (%s)', strtoupper($item->state->getStateTextTranslated()), $this->translate('has active checks disabled') ) ]); } } return $icon; } } icingadb-web-1.4.0/library/Icingadb/View/CommentRenderer.php000066400000000000000000000120101516124654200237070ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Common\HostLink; use Icinga\Module\Icingadb\Common\Icons; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\ServiceLink; use Icinga\Module\Icingadb\Common\TicketLinks; use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Widget\MarkdownLine; use ipl\Html\Attributes; use ipl\Html\FormattedString; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Web\Common\ItemRenderer; use ipl\Web\Widget\Ball; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; use ipl\Web\Widget\TimeAgo; use ipl\Web\Widget\TimeUntil; /** @implements ItemRenderer */ class CommentRenderer implements ItemRenderer { use Translation; use TicketLinks; use HostLink; use ServiceLink; /** @var bool Whether the object link for th item should be omitted */ protected $noObjectLink = false; /** @var bool Whether item's subject should be a link */ protected $noSubjectLink = false; /** * Set whether the object link for th item should be omitted * * @param bool $state * * @return $this */ public function setNoObjectLink(bool $state = true): self { $this->noObjectLink = $state; return $this; } /** * Set whether item's subject should be a link * * @param bool $state * * @return $this */ public function setNoSubjectLink(bool $state = true): self { $this->noSubjectLink = $state; return $this; } public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->get('class')->addValue('comment'); } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { $visual->addHtml( (new Ball($layout === 'minimal' ? Ball::SIZE_BIG : Ball::SIZE_LARGE)) ->addAttributes(['class' => 'user-ball']) ->addHtml(Text::create($item->author[0])) ); } public function assembleTitle($item, HtmlDocument $title, string $layout): void { $isAck = $item->entry_type === 'ack'; $expires = $item->expire_time; $subjectText = sprintf( $isAck ? $this->translate('%s acknowledged', '..') : $this->translate('%s commented', '..'), $item->author ); $headerParts = [ new Icon(Icons::USER), $layout === 'header' || $this->noSubjectLink ? new HtmlElement('span', Attributes::create(['class' => 'subject']), Text::create($subjectText)) : new Link($subjectText, Links::comment($item), ['class' => 'subject']) ]; if ($isAck) { $label = [Text::create('ack')]; if ($item->is_persistent) { array_unshift($label, new Icon(Icons::IS_PERSISTENT)); } $headerParts[] = Text::create(' '); $headerParts[] = new HtmlElement('span', Attributes::create(['class' => 'ack-badge badge']), ...$label); } if ($expires !== null) { $headerParts[] = Text::create(' '); $headerParts[] = new HtmlElement( 'span', Attributes::create(['class' => 'ack-badge badge']), Text::create($this->translate('EXPIRES')) ); } if ($this->noObjectLink) { // pass } elseif ($item->object_type === 'host') { $headerParts[] = $this->createHostLink($item->host, true); } else { $headerParts[] = $this->createServiceLink($item->service, $item->service->host, true); } $title->addHtml(...$headerParts); } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { $markdownLine = new MarkdownLine($this->createTicketLinks($item->text)); $caption->getAttributes()->add($markdownLine->getAttributes()); $caption->addFrom($markdownLine); } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { if ($item->expire_time) { $info->addHtml( FormattedString::create( $this->translate("expires %s"), new TimeUntil($item->expire_time->getTimestamp()) ) ); } else { $info->addHtml( FormattedString::create( $this->translate("created %s"), new TimeAgo($item->entry_time->getTimestamp()) ) ); } } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { return false; // no custom sections } } icingadb-web-1.4.0/library/Icingadb/View/DowntimeRenderer.php000066400000000000000000000167741516124654200241200ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Date\DateFormatter; use Icinga\Module\Icingadb\Common\HostLink; use Icinga\Module\Icingadb\Common\Icons; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\ServiceLink; use Icinga\Module\Icingadb\Common\TicketLinks; use Icinga\Module\Icingadb\Model\Downtime; use Icinga\Module\Icingadb\Widget\MarkdownLine; use ipl\Html\Attributes; use ipl\Html\Html; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\TemplateString; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Web\Common\ItemRenderer; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; /** @implements ItemRenderer */ class DowntimeRenderer implements ItemRenderer { use Translation; use TicketLinks; use HostLink; use ServiceLink; /** @var int Current Time */ protected $currentTime; /** @var int Duration */ protected $duration; /** @var int Downtime end time */ protected $endTime; /** @var bool Whether the downtime is active */ protected $isActive; /** @var int Downtime start time */ protected $startTime; /** @var bool Whether the state has been loaded */ protected $stateLoaded = false; /** @var bool Whether the object link for th item should be omitted */ protected $noObjectLink = false; /** * Set whether the object link for th item should be omitted * * @param bool $state * * @return $this */ public function setNoObjectLink(bool $state = true): self { $this->noObjectLink = $state; return $this; } /** * Load the state of the downtime * * @param Downtime $item * * @return void */ protected function loadState(Downtime $item): void { if ($this->stateLoaded) { return; } if ( isset($item->start_time, $item->end_time) && $item->is_flexible && $item->is_in_effect ) { $this->startTime = $item->start_time->getTimestamp(); $this->endTime = $item->end_time->getTimestamp(); } else { $this->startTime = $item->scheduled_start_time->getTimestamp(); $this->endTime = $item->scheduled_end_time->getTimestamp(); } $this->currentTime = time(); $this->isActive = $item->is_in_effect || ($item->is_flexible && $item->scheduled_start_time->getTimestamp() <= $this->currentTime); $until = ($this->isActive ? $this->endTime : $this->startTime) - $this->currentTime; $this->duration = explode(' ', DateFormatter::formatDuration( $until <= 3600 ? $until : $until + (3600 - ((int) $until % 3600)) ), 2)[0]; $this->stateLoaded = true; } public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->add(new Attributes(['class' => ['downtime', $item->is_in_effect ? 'in-effect' : '']])); } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { $this->loadState($item); $dateTime = DateFormatter::formatDateTime($this->endTime); if ($this->isActive) { $visual->addHtml(Html::sprintf( $this->translate('%s left', '..'), Html::tag( 'strong', Html::tag( 'time', [ 'datetime' => $dateTime, 'title' => $dateTime ], $this->duration ) ) )); } else { $visual->addHtml(Html::sprintf( $this->translate('in %s', '..'), Html::tag('strong', $this->duration) )); } } public function assembleTitle($item, HtmlDocument $title, string $layout): void { if ($this->noObjectLink) { $link = null; } elseif ($item->object_type === 'host') { $link = $this->createHostLink($item->host, true); } else { $link = $this->createServiceLink($item->service, $item->service->host, true); } if ($item->is_flexible) { if ($link !== null) { $template = $this->translate('{{#link}}Flexible Downtime{{/link}} for %s'); } else { $template = $this->translate('Flexible Downtime'); } } else { if ($link !== null) { $template = $this->translate('{{#link}}Fixed Downtime{{/link}} for %s'); } else { $template = $this->translate('Fixed Downtime'); } } if ($layout === 'header') { if ($link === null) { $title->addHtml(HtmlElement::create('span', [ 'class' => 'subject'], $template)); } else { $title->addHtml(TemplateString::create( $template, ['link' => HtmlElement::create('span', [ 'class' => 'subject'])], $link )); } } else { if ($link === null) { $title->addHtml(new Link($template, Links::downtime($item))); } else { $title->addHtml(TemplateString::create( $template, ['link' => new Link('', Links::downtime($item))], $link )); } } } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { $markdownLine = new MarkdownLine($this->createTicketLinks($item->comment)); $caption->getAttributes()->add($markdownLine->getAttributes()); $caption->addHtml( new HtmlElement( 'span', null, new Icon(Icons::USER), Text::create($item->author) ), Text::create(': ') )->addFrom($markdownLine); } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { $this->loadState($item); $dateTime = DateFormatter::formatDateTime($this->isActive ? $this->endTime : $this->startTime); $info->addHtml(Html::tag( 'time', [ 'datetime' => $dateTime, 'title' => $dateTime ], sprintf( $this->isActive ? $this->translate('expires in %s', '..') : $this->translate('starts in %s', '..'), $this->duration ) )); } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { if ($name === 'progress' && ($layout === 'detailed' || $layout === 'common')) { $this->loadState($item); $element ->addAttributes(Attributes::create([ 'data-animate-progress' => true, 'data-start-time' => $this->startTime, 'data-end-time' => $this->endTime ])) ->addHtml(new HtmlElement('div', Attributes::create(['class' => 'bar']))); return true; } return false; } } icingadb-web-1.4.0/library/Icingadb/View/EventRenderer.php000066400000000000000000000402601516124654200233760ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Date\DateFormatter; use Icinga\Module\Icingadb\Common\HostLink; use Icinga\Module\Icingadb\Common\HostStates; use Icinga\Module\Icingadb\Common\Icons; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\ServiceLink; use Icinga\Module\Icingadb\Common\ServiceStates; use Icinga\Module\Icingadb\Common\TicketLinks; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Util\PluginOutput; use Icinga\Module\Icingadb\Widget\CheckAttempt; use Icinga\Module\Icingadb\Widget\MarkdownLine; use Icinga\Module\Icingadb\Widget\PluginOutputContainer; use Icinga\Module\Icingadb\Widget\StateChange; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Web\Common\ItemRenderer; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBall; use ipl\Web\Widget\TimeAgo; /** @implements ItemRenderer */ class EventRenderer implements ItemRenderer { use Translation; use TicketLinks; use HostLink; use ServiceLink; /** @var NotificationRenderer To render NotificationHistory event */ protected $notificationRenderer; public function __construct() { $this->notificationRenderer = new NotificationRenderer(); } public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->get('class')->addValue('history'); } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { $ballSize = StateBall::SIZE_LARGE; if ($layout === 'minimal' || $layout === 'header') { $ballSize = StateBall::SIZE_BIG; } switch ($item->event_type) { case 'comment_add': $visual->addHtml( HtmlElement::create( 'div', ['class' => ['icon-ball', 'ball-size-' . $ballSize]], new Icon(Icons::COMMENT) ) ); break; case 'comment_remove': case 'downtime_end': case 'ack_clear': $visual->addHtml( HtmlElement::create( 'div', ['class' => ['icon-ball', 'ball-size-' . $ballSize]], new Icon(Icons::REMOVE) ) ); break; case 'downtime_start': $visual->addHtml( HtmlElement::create( 'div', ['class' => ['icon-ball', 'ball-size-' . $ballSize]], new Icon(Icons::IN_DOWNTIME) ) ); break; case 'ack_set': $visual->addHtml( HtmlElement::create( 'div', ['class' => ['icon-ball', 'ball-size-' . $ballSize]], new Icon(Icons::IS_ACKNOWLEDGED) ) ); break; case 'flapping_end': case 'flapping_start': $visual->addHtml( HtmlElement::create( 'div', ['class' => ['icon-ball', 'ball-size-' . $ballSize]], new Icon(Icons::IS_FLAPPING) ) ); break; case 'notification': $visual->addHtml( HtmlElement::create( 'div', ['class' => ['icon-ball', 'ball-size-' . $ballSize]], new Icon(Icons::NOTIFICATION) ) ); break; case 'state_change': if ($item->state->state_type === 'soft') { $stateType = 'soft_state'; $previousStateType = 'previous_soft_state'; if ($item->state->previous_soft_state === 0) { $previousStateType = 'hard_state'; } if ($layout !== 'minimal' && $layout !== 'header') { $visual->addHtml( new CheckAttempt( (int) $item->state->check_attempt, (int) $item->state->max_check_attempts ) ); } } else { $stateType = 'hard_state'; $previousStateType = 'previous_hard_state'; if ($item->state->hard_state === $item->state->previous_hard_state) { $previousStateType = 'previous_soft_state'; } } if ($item->object_type === 'host') { $state = HostStates::text($item->state->$stateType); $previousState = HostStates::text($item->state->$previousStateType); } else { $state = ServiceStates::text($item->state->$stateType); $previousState = ServiceStates::text($item->state->$previousStateType); } $stateChange = new StateChange($state, $previousState); if ($stateType === 'soft_state') { $stateChange->setCurrentStateBallSize(StateBall::SIZE_MEDIUM_LARGE); } if ($previousStateType === 'previous_soft_state') { $stateChange->setPreviousStateBallSize(StateBall::SIZE_MEDIUM_LARGE); if ($stateType === 'soft_state') { $visual->getAttributes()->add('class', 'small-state-change'); } } $visual->prependHtml($stateChange); break; } } public function assembleTitle($item, HtmlDocument $title, string $layout): void { if ($item->event_type === 'notification' && isset($item->notification->id)) { $item->notification->history = $item; $item->notification->host = $item->host; $item->notification->service = $item->service; $this->notificationRenderer->assembleTitle($item->notification, $title, $layout); return; } switch ($item->event_type) { case 'comment_add': $subjectLabel = $this->translate('Comment added'); break; case 'comment_remove': if (! empty($item->comment->removed_by)) { if ($item->comment->removed_by !== $item->comment->author) { $subjectLabel = sprintf( $this->translate('Comment removed by %s', '..'), $item->comment->removed_by ); } else { $subjectLabel = $this->translate('Comment removed by author'); } } elseif (isset($item->comment->expire_time)) { $subjectLabel = $this->translate('Comment expired'); } else { $subjectLabel = $this->translate('Comment removed'); } break; case 'downtime_end': if (! empty($item->downtime->cancelled_by)) { if ($item->downtime->cancelled_by !== $item->downtime->author) { $subjectLabel = sprintf( $this->translate('Downtime cancelled by %s', '..'), $item->downtime->cancelled_by ); } else { $subjectLabel = $this->translate('Downtime cancelled by author'); } } elseif ($item->downtime->has_been_cancelled === 'y') { $subjectLabel = $this->translate('Downtime cancelled'); } else { $subjectLabel = $this->translate('Downtime ended'); } break; case 'downtime_start': $subjectLabel = $this->translate('Downtime started'); break; case 'flapping_start': $subjectLabel = $this->translate('Flapping started'); break; case 'flapping_end': $subjectLabel = $this->translate('Flapping stopped'); break; case 'ack_set': $subjectLabel = $this->translate('Acknowledgement set'); break; case 'ack_clear': if (! empty($item->acknowledgement->cleared_by)) { if ($item->acknowledgement->cleared_by !== $item->acknowledgement->author) { $subjectLabel = sprintf( $this->translate('Acknowledgement cleared by %s', '..'), $item->acknowledgement->cleared_by ); } else { $subjectLabel = $this->translate('Acknowledgement cleared by author'); } } elseif (isset($item->acknowledgement->expire_time)) { $subjectLabel = $this->translate('Acknowledgement expired'); } else { $subjectLabel = $this->translate('Acknowledgement cleared'); } break; case 'state_change': $state = $item->state->state_type === 'hard' ? $item->state->hard_state : $item->state->soft_state; if ($state === 0) { if ($item->object_type === 'service') { $subjectLabel = $this->translate('Service recovered'); } else { $subjectLabel = $this->translate('Host recovered'); } } else { if ($item->state->state_type === 'hard') { $subjectLabel = $this->translate('Hard state changed'); } else { $subjectLabel = $this->translate('Soft state changed'); } } break; default: $subjectLabel = $item->event_type; break; } if ($layout === 'header') { $title->addHtml(HtmlElement::create('span', ['class' => 'subject'], $subjectLabel)); } else { $title->addHtml(new Link($subjectLabel, Links::event($item), ['class' => 'subject'])); } if ($item->object_type === 'host' && isset($item->host->id)) { $link = $this->createHostLink($item->host, true); } elseif (isset($item->host->id, $item->service->id)) { $link = $this->createServiceLink($item->service, $item->host, true); } $title->addHtml(Text::create(' ')); if (isset($link)) { $title->addHtml($link); } } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { if ($item->event_type === 'notification') { $item->notification->host = $item->host; $item->notification->service = $item->service; $this->notificationRenderer->assembleCaption($item->notification, $caption, $layout); return; } switch ($item->event_type) { case 'comment_add': case 'comment_remove': $markdownLine = new MarkdownLine($this->createTicketLinks($item->comment->comment)); $caption->getAttributes()->add($markdownLine->getAttributes()); $caption->add([ new Icon(Icons::USER), $item->comment->author, ': ' ])->addFrom($markdownLine); break; case 'downtime_end': case 'downtime_start': $markdownLine = new MarkdownLine($this->createTicketLinks($item->downtime->comment)); $caption->getAttributes()->add($markdownLine->getAttributes()); $caption->add([ new Icon(Icons::USER), $item->downtime->author, ': ' ])->addFrom($markdownLine); break; case 'flapping_start': $caption ->add( sprintf( $this->translate('State Change Rate: %.2f%%; Start Threshold: %.2f%%'), $item->flapping->percent_state_change_start, $item->flapping->flapping_threshold_high ) ) ->getAttributes() ->add('class', 'plugin-output'); break; case 'flapping_end': $caption ->add( sprintf( $this->translate('State Change Rate: %.2f%%; End Threshold: %.2f%%; Flapping for %s'), $item->flapping->percent_state_change_end, $item->flapping->flapping_threshold_low, isset($item->flapping->end_time) ? DateFormatter::formatDuration( $item->flapping->end_time->getTimestamp() - $item->flapping->start_time->getTimestamp() ) : $this->translate('n. a.') ) ) ->getAttributes() ->add('class', 'plugin-output'); break; case 'ack_clear': case 'ack_set': if (! isset($item->acknowledgement->comment) && ! isset($item->acknowledgement->author)) { $caption->addHtml( new EmptyState( $this->translate('This acknowledgement was set before Icinga DB history recording') ) ); } else { $markdownLine = new MarkdownLine($this->createTicketLinks($item->acknowledgement->comment)); $caption->getAttributes()->add($markdownLine->getAttributes()); $caption->add([ new Icon(Icons::USER), $item->acknowledgement->author, ': ' ])->addFrom($markdownLine); } break; case 'state_change': $commandName = $item->object_type === 'host' ? $item->host->checkcommand_name : $item->service->checkcommand_name; if (isset($commandName)) { if (empty($item->state->output)) { $caption->addHtml(new EmptyState($this->translate('Output unavailable.'))); } else { $caption->addHtml( new PluginOutputContainer( (new PluginOutput($item->state->output)) ->setCommandName($commandName) ) ); } } else { $caption->addHtml( new EmptyState($this->translate('Waiting for Icinga DB to synchronize the config.')) ); } break; } } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { $info->addHtml(new TimeAgo($item->event_time->getTimestamp())); } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { return false; // no custom sections } } icingadb-web-1.4.0/library/Icingadb/View/HostRenderer.php000066400000000000000000000020001516124654200232200ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Host; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Html\ValidHtml; use ipl\Web\Widget\Link; /** @extends BaseHostAndServiceRenderer */ class HostRenderer extends BaseHostAndServiceRenderer { public function assembleAttributes($item, Attributes $attributes, string $layout): void { parent::assembleAttributes($item, $attributes, $layout); $attributes->get('class')->addValue('host'); } protected function createSubject($item, string $layout): ValidHtml { if ($layout === 'header') { return new HtmlElement('span', new Attributes(['class' => 'subject']), new Text($item->display_name)); } return new Link($item->display_name, Links::host($item), ['class' => 'subject']); } } icingadb-web-1.4.0/library/Icingadb/View/HostgroupGridRenderer.php000066400000000000000000000137051516124654200251210ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Hostgroupsummary; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Stdlib\BaseFilter; use ipl\Stdlib\Filter; use ipl\Web\Common\ItemRenderer; use ipl\Web\Url; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBadge; /** @implements ItemRenderer */ class HostgroupGridRenderer implements ItemRenderer { use Translation; use BaseFilter; public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->get('class')->addValue(['object-grid-cell', 'hostgroup']); } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { $url = Url::fromPath('icingadb/hosts'); $urlFilter = Filter::all(Filter::equal('hostgroup.name', $item->name)); if ($item->hosts_down_unhandled > 0) { $urlFilter->add(Filter::equal('host.state.soft_state', 1)) ->add(Filter::equal('host.state.is_handled', 'n')) ->add(Filter::equal('host.state.is_reachable', 'y')); $link = new Link( new StateBadge($item->hosts_down_unhandled, 'down'), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d host that is currently in DOWN state in host group "%s"', 'List %d hosts which are currently in DOWN state in host group "%s"', $item->hosts_down_unhandled ), $item->hosts_down_unhandled, $item->display_name ) ] ); } elseif ($item->hosts_down_handled > 0) { $urlFilter->add(Filter::equal('host.state.soft_state', 1)) ->add(Filter::any( Filter::equal('host.state.is_handled', 'y'), Filter::equal('host.state.is_reachable', 'n') )); $link = new Link( new StateBadge($item->hosts_down_handled, 'down', true), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d host that is currently in DOWN (Acknowledged) state in host group "%s"', 'List %d hosts which are currently in DOWN (Acknowledged) state in host group "%s"', $item->hosts_down_handled ), $item->hosts_down_handled, $item->display_name ) ] ); } elseif ($item->hosts_pending > 0) { $urlFilter->add(Filter::equal('host.state.soft_state', 99)); $link = new Link( new StateBadge($item->hosts_pending, 'pending'), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d host that is currently in PENDING state in host group "%s"', 'List %d hosts which are currently in PENDING state in host group "%s"', $item->hosts_pending ), $item->hosts_pending, $item->display_name ) ] ); } elseif ($item->hosts_up > 0) { $urlFilter->add(Filter::equal('host.state.soft_state', 0)); $link = new Link( new StateBadge($item->hosts_up, 'up'), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d host that is currently in UP state in host group "%s"', 'List %d hosts which are currently in UP state in host group "%s"', $item->hosts_up ), $item->hosts_up, $item->display_name ) ] ); } else { $link = new Link( new StateBadge(0, 'none'), Links::hostgroup($item), [ 'title' => sprintf( $this->translate('There are no hosts in host group "%s"'), $item->display_name ) ] ); } $visual->addHtml($link); } public function assembleTitle($item, HtmlDocument $title, string $layout): void { $link = new Link( $item->display_name, Links::hostgroup($item), [ 'class' => 'subject', 'title' => sprintf( $this->translate('List all hosts in the group "%s"'), $item->display_name ) ] ); if ($this->hasBaseFilter()) { $link->getUrl()->setFilter($this->getBaseFilter()); } $title->addHtml($link); } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { $caption->addHtml(Text::create($item->name)); } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { return false; // no custom sections } } icingadb-web-1.4.0/library/Icingadb/View/HostgroupRenderer.php000066400000000000000000000070441516124654200243120ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Hostgroupsummary; use Icinga\Module\Icingadb\Widget\Detail\HostStatistics; use Icinga\Module\Icingadb\Widget\Detail\ServiceStatistics; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Stdlib\BaseFilter; use ipl\Stdlib\Filter; use ipl\Web\Widget\ItemTable\ItemTableRenderer; use ipl\Web\Widget\Link; /** @implements ItemTableRenderer */ class HostgroupRenderer implements ItemTableRenderer { use Translation; use BaseFilter; public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->get('class')->addValue('hostgroup'); } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { } public function assembleTitle($item, HtmlDocument $title, string $layout): void { if ($layout === 'header') { $title->addHtml(new HtmlElement( 'span', Attributes::create(['class' => 'subject']), Text::create($item->display_name) )); } else { $link = new Link( $item->display_name, Links::hostgroup($item), [ 'class' => 'subject', 'title' => sprintf( $this->translate('List all hosts in the group "%s"'), $item->display_name ) ] ); if ($this->hasBaseFilter()) { $link->getUrl()->setFilter($this->getBaseFilter()); } $title->addHtml($link); } } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { $caption->addHtml(Text::create($item->name)); } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { // assembleExtendedInfo() is only called when $layout == header $info->addHtml(...$this->createStatistics($item)); } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { return false; // no custom sections } public function assembleColumns($item, HtmlDocument $columns, string $layout): void { [$hostStats, $serviceStats] = $this->createStatistics($item); if ($this->hasBaseFilter()) { $hostStats->setBaseFilter(Filter::all($hostStats->getBaseFilter(), $this->getBaseFilter())); $serviceStats->setBaseFilter(Filter::all($serviceStats->getBaseFilter(), $this->getBaseFilter())); } $columns->addHtml($hostStats, $serviceStats); } /** * Create statistics for the given item * * @param Hostgroupsummary $item * * @return array{0: HostStatistics, 1: ServiceStatistics} */ protected function createStatistics(Hostgroupsummary $item): array { $hostStats = (new HostStatistics($item)) ->setBaseFilter(Filter::equal('hostgroup.name', $item->name)); $serviceStats = (new ServiceStatistics($item)) ->setBaseFilter(Filter::equal('hostgroup.name', $item->name)); return [$hostStats, $serviceStats]; } } icingadb-web-1.4.0/library/Icingadb/View/NotificationRenderer.php000066400000000000000000000156061516124654200247510ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Common\HostLink; use Icinga\Module\Icingadb\Common\HostStates; use Icinga\Module\Icingadb\Common\Icons; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\ServiceLink; use Icinga\Module\Icingadb\Common\ServiceStates; use Icinga\Module\Icingadb\Model\NotificationHistory; use Icinga\Module\Icingadb\Util\PluginOutput; use Icinga\Module\Icingadb\Widget\PluginOutputContainer; use Icinga\Module\Icingadb\Widget\StateChange; use InvalidArgumentException; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Web\Common\ItemRenderer; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBall; use ipl\Web\Widget\TimeAgo; /** @implements ItemRenderer */ class NotificationRenderer implements ItemRenderer { use Translation; use HostLink; use ServiceLink; public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->get('class')->addValue('notification'); } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { $ballSize = StateBall::SIZE_LARGE; if ($layout === 'minimal' || $layout === 'header') { $ballSize = StateBall::SIZE_BIG; } switch ($item->type) { case 'acknowledgement': $visual->addHtml(HtmlElement::create( 'div', ['class' => ['icon-ball', 'ball-size-' . $ballSize]], new Icon(Icons::IS_ACKNOWLEDGED) )); break; case 'custom': $visual->addHtml(HtmlElement::create( 'div', ['class' => ['icon-ball', 'ball-size-' . $ballSize]], new Icon(Icons::NOTIFICATION) )); break; case 'downtime_end': case 'downtime_removed': case 'downtime_start': $visual->addHtml(HtmlElement::create( 'div', ['class' => ['icon-ball', 'ball-size-' . $ballSize]], new Icon(Icons::IN_DOWNTIME) )); break; case 'flapping_end': case 'flapping_start': $visual->addHtml(HtmlElement::create( 'div', ['class' => ['icon-ball', 'ball-size-' . $ballSize]], new Icon(Icons::IS_FLAPPING) )); break; case 'problem': case 'recovery': if ($item->object_type === 'host') { $state = HostStates::text($item->state); $previousHardState = HostStates::text($item->previous_hard_state); } else { $state = ServiceStates::text($item->state); $previousHardState = ServiceStates::text($item->previous_hard_state); } $visual->addHtml(new StateChange($state, $previousHardState)); break; } } public function assembleTitle($item, HtmlDocument $title, string $layout): void { if ($layout === 'header') { $title->addHtml(HtmlElement::create( 'span', ['class' => 'subject'], sprintf($this->phraseForType($item->type), ucfirst($item->object_type)) )); } else { $title->addHtml(new Link( sprintf($this->phraseForType($item->type), ucfirst($item->object_type)), Links::event($item->history), ['class' => 'subject'] )); } if ($item->object_type === 'host') { $link = $this->createHostLink($item->host, true); } else { $link = $this->createServiceLink($item->service, $item->host, true); } $title->addHtml(Text::create(' '), $link); } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { if (in_array($item->type, ['flapping_end', 'flapping_start', 'problem', 'recovery'])) { $commandName = $item->object_type === 'host' ? $item->host->checkcommand_name : $item->service->checkcommand_name; if (isset($commandName)) { if (empty($item->text)) { $caption->addHtml(new EmptyState($this->translate('Output unavailable.'))); } else { $caption->addHtml(new PluginOutputContainer( (new PluginOutput($item->text)) ->setCommandName($commandName) )); } } else { $caption->addHtml(new EmptyState($this->translate('Waiting for Icinga DB to synchronize the config.'))); } } else { $caption->add([ new Icon(Icons::USER), $item->author, ': ', $item->text ]); } } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { $info->addHtml(new TimeAgo($item->send_time->getTimestamp())); } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { return false; // no custom sections } /** * Get a localized phrase for the given notification type * * @param string $type * * @return string */ protected function phraseForType(string $type): string { switch ($type) { case 'acknowledgement': return $this->translate('Problem acknowledged'); case 'custom': return $this->translate('Custom Notification triggered'); case 'downtime_end': return $this->translate('Downtime ended'); case 'downtime_removed': return $this->translate('Downtime removed'); case 'downtime_start': return $this->translate('Downtime started'); case 'flapping_end': return $this->translate('Flapping stopped'); case 'flapping_start': return $this->translate('Flapping started'); case 'problem': return $this->translate('%s ran into a problem'); case 'recovery': return $this->translate('%s recovered'); default: throw new InvalidArgumentException(sprintf('Type %s is not a valid notification type', $type)); } } } icingadb-web-1.4.0/library/Icingadb/View/RedundancyGroupRenderer.php000066400000000000000000000055001516124654200254240ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Model\RedundancyGroup; use Icinga\Module\Icingadb\Widget\DependencyNodeStatistics; use ipl\Html\Attributes; use ipl\Html\Html; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Web\Common\ItemRenderer; use ipl\Web\Url; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBall; use ipl\Web\Widget\TimeSince; /** @implements ItemRenderer */ class RedundancyGroupRenderer implements ItemRenderer { use Translation; public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->get('class')->addValue('redundancy-group'); } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { $ballSize = StateBall::SIZE_LARGE; if ($layout === 'minimal' || $layout === 'header') { $ballSize = StateBall::SIZE_BIG; } $stateBall = new StateBall($item->state->getStateText(), $ballSize); $stateBall->add($item->state->getIcon()); $visual->addHtml($stateBall); } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { $caption->addHtml(new DependencyNodeStatistics($item->summary)); } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { } public function assembleTitle($item, HtmlDocument $title, string $layout): void { if ($layout === 'header') { $subject = new HtmlElement( 'span', Attributes::create(['class' => 'subject']), Text::create($item->display_name) ); } else { $subject = new Link( $item->display_name, Url::fromPath('icingadb/redundancygroup', ['id' => bin2hex($item->id)]), ['class' => 'subject'] ); } if ($item->state->failed) { $title->addHtml(Html::sprintf( $this->translate('%s has no working objects', ' has ...'), $subject )); } else { $title->addHtml(Html::sprintf( $this->translate('%s has working objects', ' has ...'), $subject )); } } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { $info->addHtml(new TimeSince($item->state->last_state_change->getTimestamp())); } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { return $name === 'icon-image'; // Always add the icon-image section } } icingadb-web-1.4.0/library/Icingadb/View/ServiceRenderer.php000066400000000000000000000027371516124654200237240ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Service; use ipl\Html\Attributes; use ipl\Html\Html; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Html\ValidHtml; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBall; /** @extends BaseHostAndServiceRenderer */ class ServiceRenderer extends BaseHostAndServiceRenderer { public function assembleAttributes($item, Attributes $attributes, string $layout): void { parent::assembleAttributes($item, $attributes, $layout); $attributes->get('class')->addValue('service'); } protected function createSubject($item, string $layout): ValidHtml { $service = $item->display_name; $host = [ new StateBall($item->host->state->getStateText(), StateBall::SIZE_MEDIUM), ' ', $item->host->display_name ]; $host = new Link($host, Links::host($item->host), ['class' => 'subject']); if ($layout === 'header') { $service = new HtmlElement('span', new Attributes(['class' => 'subject']), new Text($service)); } else { $service = new Link($service, Links::service($item, $item->host), ['class' => 'subject']); } return Html::sprintf($this->translate('%s on %s', ' on '), $service, $host); } } icingadb-web-1.4.0/library/Icingadb/View/ServicegroupGridRenderer.php000066400000000000000000000246031516124654200256030ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\ServicegroupSummary; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Stdlib\BaseFilter; use ipl\Stdlib\Filter; use ipl\Web\Common\ItemRenderer; use ipl\Web\Url; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBadge; /** @implements ItemRenderer */ class ServicegroupGridRenderer implements ItemRenderer { use Translation; use BaseFilter; public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->get('class')->addValue(['object-grid-cell', 'servicegroup']); } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { $url = Url::fromPath('icingadb/services/grid'); $urlFilter = Filter::all(Filter::equal('servicegroup.name', $item->name)); if ($item->services_critical_unhandled > 0) { $urlFilter->add(Filter::equal('service.state.soft_state', 2)) ->add(Filter::equal('service.state.is_handled', 'n')) ->add(Filter::equal('service.state.is_reachable', 'y')); $link = new Link( new StateBadge($item->services_critical_unhandled, 'critical'), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d service that is currently in CRITICAL state in service group "%s"', 'List %d services which are currently in CRITICAL state in service group "%s"', $item->services_critical_unhandled ), $item->services_critical_unhandled, $item->display_name ) ] ); } elseif ($item->services_critical_handled > 0) { $urlFilter->add(Filter::equal('service.state.soft_state', 2)) ->add(Filter::any( Filter::equal('service.state.is_handled', 'y'), Filter::equal('service.state.is_reachable', 'n') )); $link = new Link( new StateBadge($item->services_critical_handled, 'critical', true), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d service that is currently in CRITICAL (Acknowledged) state in service group' . ' "%s"', 'List %d services which are currently in CRITICAL (Acknowledged) state in service group' . ' "%s"', $item->services_critical_handled ), $item->services_critical_handled, $item->display_name ) ] ); } elseif ($item->services_warning_unhandled > 0) { $urlFilter->add(Filter::equal('service.state.soft_state', 1)) ->add(Filter::equal('service.state.is_handled', 'n')) ->add(Filter::equal('service.state.is_reachable', 'y')); $link = new Link( new StateBadge($item->services_warning_unhandled, 'warning'), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d service that is currently in WARNING state in service group "%s"', 'List %d services which are currently in WARNING state in service group "%s"', $item->services_warning_unhandled ), $item->services_warning_unhandled, $item->display_name ) ] ); } elseif ($item->services_warning_handled > 0) { $urlFilter->add(Filter::equal('service.state.soft_state', 1)) ->add(Filter::any( Filter::equal('service.state.is_handled', 'y'), Filter::equal('service.state.is_reachable', 'n') )); $link = new Link( new StateBadge($item->services_warning_handled, 'warning', true), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d service that is currently in WARNING (Acknowledged) state in service group' . ' "%s"', 'List %d services which are currently in WARNING (Acknowledged) state in service group' . ' "%s"', $item->services_warning_handled ), $item->services_warning_handled, $item->display_name ) ] ); } elseif ($item->services_unknown_unhandled > 0) { $urlFilter->add(Filter::equal('service.state.soft_state', 3)) ->add(Filter::equal('service.state.is_handled', 'n')) ->add(Filter::equal('service.state.is_reachable', 'y')); $link = new Link( new StateBadge($item->services_unknown_unhandled, 'unknown'), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d service that is currently in UNKNOWN state in service group "%s"', 'List %d services which are currently in UNKNOWN state in service group "%s"', $item->services_unknown_unhandled ), $item->services_unknown_unhandled, $item->display_name ) ] ); } elseif ($item->services_unknown_handled > 0) { $urlFilter->add(Filter::equal('service.state.soft_state', 3)) ->add(Filter::any( Filter::equal('service.state.is_handled', 'y'), Filter::equal('service.state.is_reachable', 'n') )); $link = new Link( new StateBadge($item->services_unknown_handled, 'unknown', true), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d service that is currently in UNKNOWN (Acknowledged) state in service group' . ' "%s"', 'List %d services which are currently in UNKNOWN (Acknowledged) state in service group' . ' "%s"', $item->services_unknown_handled ), $item->services_unknown_handled, $item->display_name ) ] ); } elseif ($item->services_pending > 0) { $urlFilter->add(Filter::equal('service.state.soft_state', 99)); $link = new Link( new StateBadge($item->services_pending, 'pending'), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d service that is currently in PENDING state in service group "%s"', 'List %d services which are currently in PENDING state in service group "%s"', $item->services_pending ), $item->services_pending, $item->display_name ) ] ); } elseif ($item->services_ok > 0) { $urlFilter->add(Filter::equal('service.state.soft_state', 0)); $link = new Link( new StateBadge($item->services_ok, 'ok'), $url->setFilter($urlFilter), [ 'title' => sprintf( $this->translatePlural( 'List %d service that is currently in OK state in service group "%s"', 'List %d services which are currently in OK state in service group "%s"', $item->services_ok ), $item->services_ok, $item->display_name ) ] ); } else { $link = new Link( new StateBadge(0, 'none'), Links::servicegroup($item), [ 'title' => sprintf( $this->translate('There are no services in service group "%s"'), $item->display_name ) ] ); } $visual->addHtml($link); } public function assembleTitle($item, HtmlDocument $title, string $layout): void { $link = new Link( $item->display_name, Links::servicegroup($item), [ 'class' => 'subject', 'title' => sprintf( $this->translate('List all services in the group "%s"'), $item->display_name ) ] ); if ($this->hasBaseFilter()) { $link->getUrl()->setFilter($this->getBaseFilter()); } $title->addHtml($link); } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { $caption->addHtml(Text::create($item->name)); } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { return false; // no custom sections } } icingadb-web-1.4.0/library/Icingadb/View/ServicegroupRenderer.php000066400000000000000000000062731516124654200250000ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\ServicegroupSummary; use Icinga\Module\Icingadb\Widget\Detail\ServiceStatistics; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Stdlib\BaseFilter; use ipl\Stdlib\Filter; use ipl\Web\Widget\ItemTable\ItemTableRenderer; use ipl\Web\Widget\Link; /** @implements ItemTableRenderer */ class ServicegroupRenderer implements ItemTableRenderer { use Translation; use BaseFilter; public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->get('class')->addValue('servicegroup'); } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { } public function assembleTitle($item, HtmlDocument $title, string $layout): void { if ($layout === 'header') { $title->addHtml(new HtmlElement( 'span', Attributes::create(['class' => 'subject']), Text::create($item->display_name) )); } else { $link = new Link( $item->display_name, Links::servicegroup($item), [ 'class' => 'subject', 'title' => sprintf( $this->translate('List all services in the group "%s"'), $item->display_name ) ] ); if ($this->hasBaseFilter()) { $link->getUrl()->setFilter($this->getBaseFilter()); } $title->addHtml($link); } } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { $caption->addHtml(Text::create($item->name)); } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { // assembleExtendedInfo() is only called when $layout == header $info->addHtml($this->createStatistics($item)); } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { return false; // no custom sections } public function assembleColumns($item, HtmlDocument $columns, string $layout): void { $serviceStats = $this->createStatistics($item); if ($this->hasBaseFilter()) { $serviceStats->setBaseFilter(Filter::all($serviceStats->getBaseFilter(), $this->getBaseFilter())); } $columns->addHtml($serviceStats); } /** * Create statistics for the given item * * @param ServicegroupSummary $item * * @return ServiceStatistics */ protected function createStatistics(ServicegroupSummary $item): ServiceStatistics { return (new ServiceStatistics($item)) ->setBaseFilter(Filter::equal('servicegroup.name', $item->name)); } } icingadb-web-1.4.0/library/Icingadb/View/UserRenderer.php000066400000000000000000000037051516124654200232360ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\User; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Web\Common\ItemRenderer; use ipl\Web\Widget\Ball; use ipl\Web\Widget\Link; /** @implements ItemRenderer */ class UserRenderer implements ItemRenderer { public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->get('class')->addValue('user'); } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { $visual->addHtml( (new Ball($layout === 'minimal' ? Ball::SIZE_BIG : Ball::SIZE_LARGE)) ->addAttributes(['class' => 'user-ball']) ->addHtml(Text::create(grapheme_substr($item->display_name, 0, 1))) ); } public function assembleTitle($item, HtmlDocument $title, string $layout): void { if ($layout === 'header') { $title->addHtml(new HtmlElement( 'span', Attributes::create(['class' => 'subject']), Text::create($item->display_name) )); } else { $title->addHtml(new Link($item->display_name, Links::user($item), ['class' => 'subject'])); } } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { $caption->addHtml(Text::create($item->name)); } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { return false; // no custom sections } } icingadb-web-1.4.0/library/Icingadb/View/UsergroupRenderer.php000066400000000000000000000037441516124654200243160ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\View; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Usergroup; use ipl\Html\Attributes; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Web\Common\ItemRenderer; use ipl\Web\Widget\Ball; use ipl\Web\Widget\Link; /** @implements ItemRenderer */ class UsergroupRenderer implements ItemRenderer { public function assembleAttributes($item, Attributes $attributes, string $layout): void { $attributes->get('class')->addValue('user-group'); } public function assembleVisual($item, HtmlDocument $visual, string $layout): void { $visual->addHtml( (new Ball($layout === 'minimal' ? Ball::SIZE_BIG : Ball::SIZE_LARGE)) ->addAttributes(['class' => 'usergroup-ball']) ->addHtml(Text::create(grapheme_substr($item->display_name, 0, 1))) ); } public function assembleTitle($item, HtmlDocument $title, string $layout): void { if ($layout === 'header') { $title->addHtml(new HtmlElement( 'span', Attributes::create(['class' => 'subject']), Text::create($item->display_name) )); } else { $title->addHtml(new Link($item->display_name, Links::usergroup($item), ['class' => 'subject'])); } } public function assembleCaption($item, HtmlDocument $caption, string $layout): void { $caption->addHtml(Text::create($item->name)); } public function assembleExtendedInfo($item, HtmlDocument $info, string $layout): void { } public function assembleFooter($item, HtmlDocument $footer, string $layout): void { } public function assemble($item, string $name, HtmlDocument $element, string $layout): bool { return false; // no custom sections } } icingadb-web-1.4.0/library/Icingadb/Web/000077500000000000000000000000001516124654200177165ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Web/Control/000077500000000000000000000000001516124654200213365ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Web/Control/GridViewModeSwitcher.php000066400000000000000000000020301516124654200261000ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web\Control; /** * View mode switcher to toggle between grid and list view */ class GridViewModeSwitcher extends ViewModeSwitcher { /** @var string Default view mode */ public const DEFAULT_VIEW_MODE = 'list'; /** @var array View mode-icon pairs */ public static $viewModes = [ 'list' => 'default', 'grid' => 'grid' ]; protected function getTitle(string $viewMode): string { $active = null; $inactive = null; switch ($viewMode) { case 'list': $active = t('List view active'); $inactive = t('Switch to list view'); break; case 'grid': $active = t('Grid view active'); $inactive = t('Switch to grid view'); break; } return $viewMode === $this->getViewMode() ? $active : $inactive; } } icingadb-web-1.4.0/library/Icingadb/Web/Control/ProblemToggle.php000066400000000000000000000030741516124654200246150ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web\Control; use ipl\Web\Common\FormUid; use ipl\Web\Compat\CompatForm; class ProblemToggle extends CompatForm { use FormUid; protected $filter; protected $protector; protected $defaultAttributes = [ 'name' => 'problem-toggle', 'class' => 'icinga-form icinga-controls inline' ]; public function __construct($filter) { $this->filter = $filter; } /** * Set callback to protect ids with * * @param callable $protector * * @return $this */ public function setIdProtector(callable $protector): self { $this->protector = $protector; return $this; } /** * Get whether the toggle is checked * * @return bool */ public function isChecked(): bool { $this->ensureAssembled(); return $this->getElement('problems')->isChecked(); } protected function assemble() { $this->addElement('checkbox', 'problems', [ 'class' => 'autosubmit', 'id' => $this->protectId('problems'), 'label' => t('Problems Only'), 'value' => $this->filter !== null ]); $this->add($this->createUidElement()); } private function protectId($id) { if (is_callable($this->protector)) { return call_user_func($this->protector, $id); } return $id; } } icingadb-web-1.4.0/library/Icingadb/Web/Control/SearchBar/000077500000000000000000000000001516124654200231705ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php000066400000000000000000000461121516124654200273460ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web\Control\SearchBar; use Generator; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Model\Behavior\ReRoute; use Icinga\Module\Icingadb\Model\CustomvarFlat; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Util\ObjectSuggestionsCursor; use ipl\Html\HtmlElement; use ipl\Orm\Exception\InvalidColumnException; use ipl\Orm\Exception\InvalidRelationException; use ipl\Orm\Model; use ipl\Orm\Query; use ipl\Orm\Relation; use ipl\Orm\Relation\BelongsToMany; use ipl\Orm\Relation\HasOne; use ipl\Orm\Resolver; use ipl\Orm\UnionModel; use ipl\Sql\Expression; use ipl\Sql\Select; use ipl\Stdlib\BaseFilter; use ipl\Stdlib\Filter; use ipl\Stdlib\Seq; use ipl\Web\Control\SearchBar\SearchException; use ipl\Web\Control\SearchBar\Suggestions; use PDO; class ObjectSuggestions extends Suggestions { use Auth; use BaseFilter; use Database; /** @var Model */ protected $model; /** @var array */ protected $customVarSources; /** @var ?array */ protected ?array $fixedColumns = null; public function __construct() { $this->customVarSources = [ 'checkcommand' => t('Checkcommand %s', '..'), 'eventcommand' => t('Eventcommand %s', '..'), 'host' => t('Host %s', '..'), 'hostgroup' => t('Hostgroup %s', '..'), 'notification' => t('Notification %s', '..'), 'notificationcommand' => t('Notificationcommand %s', '..'), 'service' => t('Service %s', '..'), 'servicegroup' => t('Servicegroup %s', '..'), 'timeperiod' => t('Timeperiod %s', '..'), 'user' => t('Contact %s', '..'), 'usergroup' => t('Contactgroup %s', '..') ]; } /** * Set the model to show suggestions for * * @param string|Model $model * * @return $this */ public function setModel($model): self { if (is_string($model)) { $model = new $model(); } $this->model = $model; return $this; } /** * Get the model to show suggestions for * * @return Model */ public function getModel(): Model { if ($this->model === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->model; } protected function shouldShowRelationFor(string $column): bool { if (strpos($column, '.vars.') !== false) { return false; } $tableName = $this->getModel()->getTableName(); $columnPath = explode('.', $column); switch (count($columnPath)) { case 3: if ($columnPath[1] !== 'state' || ! in_array($tableName, ['host', 'service'])) { return true; } // For host/service state relation columns apply the same rules case 2: return $columnPath[0] !== $tableName; default: return true; } } private function applyBaseFilter(Query $query): void { $this->applyRestrictions($query); if ($this->hasBaseFilter()) { $query->filter($this->getBaseFilter()); } } protected function createQuickSearchFilter($searchTerm) { $model = $this->getModel(); $resolver = $model::on($this->getDb())->getResolver(); $quickFilter = Filter::any(); foreach ($model->getSearchColumns() as $column) { if (strpos($column, '.') === false) { $column = $resolver->qualifyColumn($column, $model->getTableName()); } $where = Filter::like($column, $searchTerm); $where->metaData()->set('columnLabel', $resolver->getColumnDefinition($column)->getLabel()); $quickFilter->add($where); } return $quickFilter; } protected function fetchValueSuggestions($column, $searchTerm, Filter\Chain $searchFilter) { $model = $this->getModel(); $query = $model::on($this->getDb()); $query->limit(static::DEFAULT_LIMIT); if (strpos($column, ' ') !== false) { // $column may be a label list($path, $_) = Seq::find( $this->fixedColumns ?? self::collectFilterColumns($query->getModel(), $query->getResolver()), $column, false ); if ($path !== null) { $column = $path; } } $columnPath = $query->getResolver()->qualifyPath($column, $model->getTableName()); list($targetPath, $columnName) = preg_split('/(?<=vars)\.|\.(?=[^.]+$)/', $columnPath, 2); $isCustomVar = false; if (substr($targetPath, -5) === '.vars') { $isCustomVar = true; $targetPath = substr($targetPath, 0, -4) . 'customvar_flat'; } if (strpos($targetPath, '.') !== false) { try { $query->with($targetPath); // TODO: Remove this, once ipl/orm does it as early } catch (InvalidRelationException $e) { throw new SearchException(sprintf(t('"%s" is not a valid relation'), $e->getRelation())); } } if ($isCustomVar) { $columnPath = $targetPath . '.flatvalue'; $query->filter(Filter::like($targetPath . '.flatname', $columnName)); } $inputFilter = Filter::like($columnPath, $searchTerm); $query->columns($columnPath); $query->orderBy($columnPath); // This had so many iterations, if it still doesn't work, consider removing it entirely :( if ($searchFilter instanceof Filter\None) { $query->filter($inputFilter); } elseif ($searchFilter instanceof Filter\All) { $searchFilter->add($inputFilter); // There may be columns part of $searchFilter which target the base table. These must be // optimized, otherwise they influence what we'll suggest to the user. (i.e. less) // The $inputFilter on the other hand must not be optimized, which it wouldn't, but since // we force optimization on its parent chain, we have to negate that. $searchFilter->metaData()->set('forceOptimization', true); $inputFilter->metaData()->set('forceOptimization', false); } else { $searchFilter = $inputFilter; } $query->filter($searchFilter); $this->applyBaseFilter($query); try { return (new ObjectSuggestionsCursor($query->getDb(), $query->assembleSelect()->distinct())) ->setFetchMode(PDO::FETCH_COLUMN); } catch (InvalidColumnException $e) { throw new SearchException(sprintf(t('"%s" is not a valid column'), $e->getColumn())); } } protected function fetchColumnSuggestions($searchTerm) { $model = $this->getModel(); $query = $model::on($this->getDb()); $parsedArrayVars = []; $exactSearchTerm = trim($searchTerm, ' *'); $exactVarSearches = []; $titleAdded = false; // Suggest exact custom variable matches first if ($exactSearchTerm !== '') { foreach ( $this->getDb()->select($this->queryCustomvarConfig( Filter::any( Filter::equal('flatname', $exactSearchTerm), Filter::like('flatname', $exactSearchTerm . '[*]') // Filter for array type custom variables ) )) as $customVar ) { $search = $name = $customVar->flatname; $exactVarSearches[] = $search; if (preg_match('/\w+(?:\[(\d*)])+$/', $search, $matches)) { $name = substr($search, 0, -(strlen($matches[1]) + 2)); if (isset($parsedArrayVars[$name])) { continue; } $parsedArrayVars[$name] = true; $search = $name . '[*]'; } foreach ($this->customVarSources as $relation => $label) { if (isset($customVar->$relation)) { if ($titleAdded === false) { $this->addHtml(HtmlElement::create( 'li', ['class' => static::SUGGESTION_TITLE_CLASS], t('Best Suggestions') )); $titleAdded = true; } yield $relation . '.vars.' . $search => sprintf($label, $name); } } } } // Ordinary columns comes after exact matches, // or if there ar no exact matches they come first $titleAdded = false; $columns = $this->fixedColumns ?? self::collectFilterColumns($query->getModel(), $query->getResolver()); foreach ($columns as $columnName => $columnMeta) { if ($this->matchSuggestion($columnName, $columnMeta, $searchTerm)) { if ($titleAdded === false) { $this->addHtml(HtmlElement::create( 'li', ['class' => static::SUGGESTION_TITLE_CLASS], t('Columns') )); $titleAdded = true; } yield $columnName => $columnMeta; } } // Finally, the other custom variable suggestions $titleAdded = false; if (! empty($exactVarSearches)) { $varFilter = Filter::all( Filter::like('flatname', $searchTerm), Filter::unequal('flatname', $exactVarSearches) ); } else { $varFilter = Filter::like('flatname', $searchTerm); } foreach ($this->getDb()->select($this->queryCustomvarConfig($varFilter)) as $customVar) { $search = $name = $customVar->flatname; if (preg_match('/\w+(?:\[(\d*)])+$/', $search, $matches)) { $name = substr($search, 0, -(strlen($matches[1]) + 2)); if (isset($parsedArrayVars[$name])) { continue; } $parsedArrayVars[$name] = true; $search = $name . '[*]'; } foreach ($this->customVarSources as $relation => $label) { if (isset($customVar->$relation)) { // Suggest exact custom variable matches first if ($titleAdded === false) { $this->addHtml(HtmlElement::create( 'li', ['class' => static::SUGGESTION_TITLE_CLASS], t('Custom Variables') )); $titleAdded = true; } yield $relation . '.vars.' . $search => sprintf($label, $name); } } } } protected function matchSuggestion($path, $label, $searchTerm) { if (preg_match('/[_.](id|bin|checksum)$/', $path)) { // Only suggest exotic columns if the user knows about them $trimmedSearch = trim($searchTerm, ' *'); return substr($path, -strlen($trimmedSearch)) === $trimmedSearch; } return parent::matchSuggestion($path, $label, $searchTerm); } /** * Create a query to fetch all available custom variables matching the given filter * * @param Filter\Rule $filter * * @return Select */ protected function queryCustomvarConfig(Filter\Rule $filter): Select { $customVars = CustomvarFlat::on($this->getDb()); $tableName = $customVars->getModel()->getTableName(); $resolver = $customVars->getResolver(); $scalarQueries = []; $aggregates = ['flatname']; foreach ($resolver->getRelations($customVars->getModel()) as $name => $relation) { if (isset($this->customVarSources[$name]) && $relation instanceof BelongsToMany) { $query = $customVars->createSubQuery( $relation->getTarget(), $resolver->qualifyPath($name, $tableName) ); $this->applyBaseFilter($query); $aggregates[$name] = new Expression("MAX($name)"); $scalarQueries[$name] = $query->assembleSelect() ->resetColumns()->columns(new Expression('1')) ->limit(1); } } $customVars->columns('flatname'); $this->applyRestrictions($customVars); $customVars->filter($filter); // applyRestrictions() does not hide protected vars, but since querying them is not possible anymore, // we have to. Otherwise, the user can choose a protected var and get an error. $protectedVarFilter = Filter::any(); foreach ($this->getAuth()->getRestrictions('icingadb/protect/variables') as $restriction) { $protectedVarFilter->add($this->parseDenylist($restriction, 'flatname')); } $customVars->filter($protectedVarFilter); $idColumn = $resolver->qualifyColumn('id', $resolver->getAlias($customVars->getModel())); $customVars = $customVars->assembleSelect(); $customVars->columns($scalarQueries); $customVars->groupBy($idColumn); $customVars->limit(static::DEFAULT_LIMIT); // This outer query exists only because there's no way to combine aggregates and sub queries (yet) return (new Select())->columns($aggregates)->from(['results' => $customVars])->groupBy('flatname'); } protected function filterColumnSuggestions($data, $searchTerm) { // Remove filtering here, as fetchColumnSuggestions already performs it yield from $data; } /** * Collect all columns of this model and its relations that can be used for filtering * * @param Model $model * @param Resolver $resolver * * @return Generator */ public static function collectFilterColumns(Model $model, Resolver $resolver): Generator { if ($model instanceof UnionModel) { $models = []; foreach ($model->getUnions() as $union) { /** @var Model $unionModel */ $unionModel = new $union[0](); $models[$unionModel->getTableName()] = $unionModel; self::collectRelations($resolver, $unionModel, $models, []); } } else { $models = [$model->getTableName() => $model]; self::collectRelations($resolver, $model, $models, []); } /** @var Model $targetModel */ foreach ($models as $path => $targetModel) { foreach ($resolver->getColumnDefinitions($targetModel) as $columnName => $definition) { yield $path . '.' . $columnName => $definition->getLabel(); } } foreach ($resolver->getBehaviors($model) as $behavior) { if ($behavior instanceof ReRoute) { foreach ($behavior->getRoutes() as $name => $route) { $relation = $resolver->resolveRelation( $resolver->qualifyPath($route, $model->getTableName()), $model ); foreach ($resolver->getColumnDefinitions($relation->getTarget()) as $columnName => $definition) { yield $name . '.' . $columnName => $definition->getLabel(); } } } } if ($model instanceof UnionModel) { $queries = $model->getUnions(); $baseModelClass = end($queries)[0]; $model = new $baseModelClass(); } $foreignMetaDataSources = []; if (! $model instanceof Host) { $foreignMetaDataSources[] = 'host.user'; $foreignMetaDataSources[] = 'host.usergroup'; } if (! $model instanceof Service) { $foreignMetaDataSources[] = 'service.user'; $foreignMetaDataSources[] = 'service.usergroup'; } foreach ($foreignMetaDataSources as $path) { $foreignColumnDefinitions = $resolver->getColumnDefinitions($resolver->resolveRelation( $resolver->qualifyPath($path, $model->getTableName()), $model )->getTarget()); foreach ($foreignColumnDefinitions as $columnName => $columnDefinition) { yield "$path.$columnName" => $columnDefinition->getLabel(); } } } /** * Collect all direct relations of the given model * * A direct relation is either a direct descendant of the model * or a descendant of such related in a to-one cardinality. * * @param Resolver $resolver * @param Model $subject * @param array $models * @param array $path */ protected static function collectRelations(Resolver $resolver, Model $subject, array &$models, array $path) { foreach ($resolver->getRelations($subject) as $name => $relation) { /** @var Relation $relation */ if ( empty($path) || ( ($name === 'state' && $path[count($path) - 1] !== 'last_comment') || $name === 'last_comment' || $name === 'notificationcommand' && $path[0] === 'notification' ) ) { $relationPath = [$name]; if ($relation instanceof HasOne && empty($path)) { array_unshift($relationPath, $subject->getTableName()); } $relationPath = array_merge($path, $relationPath); $models[join('.', $relationPath)] = $relation->getTarget(); self::collectRelations($resolver, $relation->getTarget(), $models, $relationPath); } } } /** * Reduce {@see $customVarSources} to only given relations to fetch variables from * * @param string[] $relations * * @return $this */ public function onlyWithCustomVarSources(array $relations): self { $this->customVarSources = array_intersect_key($this->customVarSources, array_flip($relations)); return $this; } /** * Provide suggestions based on a fixed set of columns * * @param array $columns * * @return $this */ public function withFixedColumns(array $columns): static { $this->fixedColumns = $columns; return $this; } } icingadb-web-1.4.0/library/Icingadb/Web/Control/ViewModeSwitcher.php000066400000000000000000000126731516124654200253100ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web\Control; use ipl\Html\Attributes; use ipl\Html\Form; use ipl\Html\FormElement\HiddenElement; use ipl\Html\FormElement\InputElement; use ipl\Html\HtmlElement; use ipl\Web\Common\FormUid; use ipl\Web\Widget\IcingaIcon; class ViewModeSwitcher extends Form { use FormUid; protected $defaultAttributes = [ 'class' => 'view-mode-switcher', 'name' => 'view-mode-switcher' ]; /** @var string Default view mode */ public const DEFAULT_VIEW_MODE = 'common'; /** @var string Default view mode param */ public const DEFAULT_VIEW_MODE_PARAM = 'view'; /** @var array View mode-icon pairs */ public static $viewModes = [ 'minimal' => 'minimal', 'common' => 'default', 'detailed' => 'detailed', 'tabular' => 'tabular' ]; /** @var string */ protected $defaultViewMode; /** @var string */ protected $method = 'POST'; /** @var callable */ protected $protector; /** @var string */ protected $viewModeParam = self::DEFAULT_VIEW_MODE_PARAM; /** * Get the default mode * * @return string */ public function getDefaultViewMode(): string { return $this->defaultViewMode ?: static::DEFAULT_VIEW_MODE; } /** * Set the default view mode * * @param string $defaultViewMode * * @return $this */ public function setDefaultViewMode(string $defaultViewMode): self { $this->defaultViewMode = $defaultViewMode; return $this; } /** * Get the view mode URL parameter * * @return string */ public function getViewModeParam(): string { return $this->viewModeParam; } /** * Set the view mode URL parameter * * @param string $viewModeParam * * @return $this */ public function setViewModeParam(string $viewModeParam): self { $this->viewModeParam = $viewModeParam; return $this; } /** * Get the view mode * * @return string */ public function getViewMode(): string { $viewMode = $this->getPopulatedValue($this->getViewModeParam(), $this->getDefaultViewMode()); // View mode stays null if explicitly populated with null. if ($viewMode !== null && array_key_exists($viewMode, static::$viewModes)) { return $viewMode; } return $this->getDefaultViewMode(); } /** * Set the view mode * * @param string $name * * @return $this */ public function setViewMode(string $name) { $this->populate([$this->getViewModeParam() => $name]); return $this; } /** * Set callback to protect ids with * * @param callable $protector * * @return $this */ public function setIdProtector(callable $protector): self { $this->protector = $protector; return $this; } private function protectId($id) { if (is_callable($this->protector)) { return call_user_func($this->protector, $id); } return $id; } protected function assemble() { $viewModeParam = $this->getViewModeParam(); $this->addElement($this->createUidElement()); $this->addElement(new HiddenElement($viewModeParam)); foreach (static::$viewModes as $viewMode => $icon) { if ($viewMode === 'tabular') { continue; } $protectedId = $this->protectId('view-mode-switcher-' . $icon); $input = new InputElement($viewModeParam, [ 'class' => 'autosubmit', 'id' => $protectedId, 'name' => $viewModeParam, 'type' => 'radio', 'value' => $viewMode ]); $input->getAttributes()->registerAttributeCallback('checked', function () use ($viewMode) { return $viewMode === $this->getViewMode(); }); $label = new HtmlElement( 'label', Attributes::create([ 'for' => $protectedId ]), new IcingaIcon($icon) ); $label->getAttributes()->registerAttributeCallback('title', function () use ($viewMode) { return $this->getTitle($viewMode); }); $this->addHtml($input, $label); } } /** * Return the title for the view mode when it is active and inactive * * @param string $viewMode * * @return string Title for the view mode when it is active and inactive */ protected function getTitle(string $viewMode): string { $active = null; $inactive = null; switch ($viewMode) { case 'minimal': $active = t('Minimal view active'); $inactive = t('Switch to minimal view'); break; case 'common': $active = t('Common view active'); $inactive = t('Switch to common view'); break; case 'detailed': $active = t('Detailed view active'); $inactive = t('Switch to detailed view'); break; } return $viewMode === $this->getViewMode() ? $active : $inactive; } } icingadb-web-1.4.0/library/Icingadb/Web/Controller.php000066400000000000000000000434201516124654200225550ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web; use Exception; use Generator; use GuzzleHttp\Psr7\ServerRequest; use Icinga\Application\Config; use Icinga\Application\Icinga; use Icinga\Application\Logger; use Icinga\Application\Version; use Icinga\Application\Web; use Icinga\Data\ConfigObject; use Icinga\Date\DateFormatter; use Icinga\Exception\ConfigurationError; use Icinga\Exception\Http\HttpBadRequestException; use Icinga\Exception\Json\JsonDecodeException; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Common\SearchControls; use Icinga\Module\Icingadb\Data\CsvResultSet; use Icinga\Module\Icingadb\Data\JsonResultSet; use Icinga\Module\Icingadb\Web\Control\GridViewModeSwitcher; use Icinga\Module\Icingadb\Web\Control\ViewModeSwitcher; use Icinga\Module\Icingadb\Widget\ItemTable\StateItemTable; use Icinga\Module\Pdfexport\PrintableHtmlDocument; use Icinga\Module\Pdfexport\ProvidedHook\Pdfexport; use Icinga\Security\SecurityException; use Icinga\User\Preferences; use Icinga\User\Preferences\PreferencesStore; use Icinga\Util\Environment; use Icinga\Util\Json; use ipl\Html\Html; use ipl\Html\ValidHtml; use ipl\Orm\Query; use ipl\Orm\UnionQuery; use ipl\Stdlib\Filter; use ipl\Web\Compat\CompatController; use ipl\Web\Control\LimitControl; use ipl\Web\Control\PaginationControl; use ipl\Web\Filter\QueryString; use ipl\Web\Url; use ipl\Web\Widget\CopyToClipboard; class Controller extends CompatController { use Auth; use Database; use SearchControls; /** @var Filter\Rule Filter from query string parameters */ private $filter; /** @var string|null */ private $format; /** @var bool */ private $formatProcessed = false; /** * Get the filter created from query string parameters * * @return Filter\Rule */ public function getFilter(): Filter\Rule { if ($this->filter === null) { $this->filter = QueryString::parse((string) $this->params); } return $this->filter; } /** * Create column control * * @param Query $query * @param ViewModeSwitcher $viewModeSwitcher * * @return array provided columns * * @throws HttpBadRequestException */ public function createColumnControl(Query $query, ViewModeSwitcher $viewModeSwitcher): array { // All of that is essentially what `ColumnControl::apply()` should do $viewMode = $this->getRequest()->getUrl()->getParam($viewModeSwitcher->getViewModeParam()); $columnsDef = $this->params->shift('columns'); if (! $columnsDef) { if ($viewMode === 'tabular') { $this->httpBadRequest('Missing parameter "columns"'); } return []; } $columns = []; foreach (explode(',', $columnsDef) as $column) { if ($column = trim($column)) { $columns[] = $column; } } // When exporting as CSV or JSON, and the user requested specific columns, only those should be included if ($this->format === 'csv' || $this->format === 'json') { $query->columns($columns); } else { $query->withColumns($columns); } if (! $viewMode) { $viewModeSwitcher->setViewMode('tabular'); } // For now this also returns the columns, but they should be accessible // by calling `ColumnControl::getColumns()` in the future return $columns; } /** * Create and return the ViewModeSwitcher * * This automatically shifts the view mode URL parameter from {@link $params}. * * @param PaginationControl $paginationControl * @param LimitControl $limitControl * @param bool $verticalPagination * * @return ViewModeSwitcher|GridViewModeSwitcher */ public function createViewModeSwitcher( PaginationControl $paginationControl, LimitControl $limitControl, bool $verticalPagination = false ): ViewModeSwitcher { $controllerName = $this->getRequest()->getControllerName(); // TODO: Make this configurable somehow. The route shouldn't be checked to choose the view modes! if ($controllerName === 'hostgroups' || $controllerName === 'servicegroups') { $viewModeSwitcher = new GridViewModeSwitcher(); } else { $viewModeSwitcher = new ViewModeSwitcher(); } $viewModeSwitcher->setIdProtector([$this->getRequest(), 'protectId']); $user = $this->Auth()->getUser(); if (($preferredModes = $user->getAdditional('icingadb.view_modes')) === null) { try { $preferredModes = Json::decode( $user->getPreferences()->getValue('icingadb', 'view_modes', '[]'), true ); } catch (JsonDecodeException $e) { Logger::error('Failed to load preferred view modes for user "%s": %s', $user->getUsername(), $e); $preferredModes = []; } $user->setAdditional('icingadb.view_modes', $preferredModes); } $requestRoute = $this->getRequest()->getUrl()->getPath(); if (isset($preferredModes[$requestRoute])) { $viewModeSwitcher->setDefaultViewMode($preferredModes[$requestRoute]); } $viewModeSwitcher->populate([ $viewModeSwitcher->getViewModeParam() => $this->params->shift($viewModeSwitcher->getViewModeParam()) ]); $session = $this->Window()->getSessionNamespace( 'icingadb-viewmode-' . $this->Window()->getContainerId() ); $viewModeSwitcher->on( ViewModeSwitcher::ON_SUCCESS, function (ViewModeSwitcher $viewModeSwitcher) use ( $user, $preferredModes, $paginationControl, $verticalPagination, &$session ) { $viewMode = $viewModeSwitcher->getValue($viewModeSwitcher->getViewModeParam()); $requestUrl = Url::fromRequest(); $preferredModes[$requestUrl->getPath()] = $viewMode; $user->setAdditional('icingadb.view_modes', $preferredModes); try { $preferencesStore = PreferencesStore::create(new ConfigObject([ 'resource' => Config::app()->get('global', 'config_resource') ]), $user); $preferencesStore->load(); $preferencesStore->save( new Preferences(['icingadb' => ['view_modes' => Json::encode($preferredModes)]]) ); } catch (Exception $e) { Logger::error('Failed to save preferred view mode for user "%s": %s', $user->getUsername(), $e); } $pageParam = $paginationControl->getPageParam(); $limitParam = LimitControl::DEFAULT_LIMIT_PARAM; $currentPage = $paginationControl->getCurrentPageNumber(); $requestUrl->setParam($viewModeSwitcher->getViewModeParam(), $viewMode); if (! $requestUrl->hasParam($limitParam)) { if ($viewMode === 'minimal' || $viewMode === 'grid') { $session->set('previous_page', $currentPage); $session->set('request_path', $requestUrl->getPath()); $limit = $paginationControl->getLimit(); if (! $verticalPagination) { // We are computing it based on the first element being rendered on this current page $currentPage = (int) (floor((($currentPage * $limit) - $limit) / ($limit * 2)) + 1); } else { $currentPage = (int) (round($currentPage * $limit / ($limit * 2))); } $session->set('current_page', $currentPage); } elseif ( $viewModeSwitcher->getDefaultViewMode() === 'minimal' || $viewModeSwitcher->getDefaultViewMode() === 'grid' ) { $limit = $paginationControl->getLimit(); if ($currentPage === $session->get('current_page')) { // No other page numbers have been selected, i.e the user only // switches back and forth without changing the page numbers $currentPage = $session->get('previous_page'); } elseif (! $verticalPagination) { $currentPage = (int) (floor((($currentPage * $limit) - $limit) / ($limit / 2)) + 1); } else { $currentPage = (int) (floor($currentPage * $limit / ($limit / 2))); } $session->clear(); } if (($requestUrl->hasParam($pageParam) && $currentPage > 1) || $currentPage > 1) { $requestUrl->setParam($pageParam, $currentPage); } else { $requestUrl->remove($pageParam); } } $this->redirectNow($requestUrl); } )->handleRequest(ServerRequest::fromGlobals()); $viewMode = $viewModeSwitcher->getViewMode(); if ($viewMode === 'minimal' || $viewMode === 'grid') { $hasLimitParam = Url::fromRequest()->hasParam($limitControl->getLimitParam()); if ($paginationControl->getDefaultPageSize() <= LimitControl::DEFAULT_LIMIT && ! $hasLimitParam) { $paginationControl->setDefaultPageSize($paginationControl->getDefaultPageSize() * 2); $limitControl->setDefaultLimit($limitControl->getDefaultLimit() * 2); $paginationControl->apply(); } } $requestPath = $session->get('request_path'); if ($requestPath && $requestPath !== $requestRoute) { $session->clear(); } return $viewModeSwitcher; } /** * Process a search request * * @param Query $query * @param array $additionalColumns * * @return void */ public function handleSearchRequest(Query $query, array $additionalColumns = []) { $q = trim($this->params->shift('q', ''), ' *'); if (! $q) { return; } $filter = Filter::any(); $this->prepareSearchFilter($query, $q, $filter, $additionalColumns); $redirectUrl = Url::fromRequest(); $redirectUrl->setParams($this->params)->setFilter($filter); $this->getResponse()->redirectAndExit($redirectUrl); } /** * Prepare the given search filter * * @param Query $query * @param string $search * @param Filter\Any $filter * @param array $additionalColumns * * @return void */ protected function prepareSearchFilter(Query $query, string $search, Filter\Any $filter, array $additionalColumns) { $columns = array_merge($query->getModel()->getSearchColumns(), $additionalColumns); foreach ($columns as $column) { if (strpos($column, '.') === false) { $column = $query->getResolver()->qualifyColumn($column, $query->getModel()->getTableName()); } $filter->add(Filter::like($column, "*$search*")); } } /** * Require permission to access the given route * * @param ?string $name If NULL, the current controller name is used * * @throws SecurityException */ public function assertRouteAccess(?string $name = null) { if (! $name) { $name = $this->getRequest()->getControllerName(); } if (! $this->isPermittedRoute($name)) { throw new SecurityException('No permission to access this route'); } } public function export(Query ...$queries) { if ($this->format === 'sql') { foreach ($queries as $query) { list($sql, $values) = $query->getDb()->getQueryBuilder()->assembleSelect($query->assembleSelect()); $unused = []; foreach ($values as $value) { $pos = strpos($sql, '?'); if ($pos !== false) { if (is_string($value)) { $value = "'" . $value . "'"; } $sql = substr_replace($sql, $value, $pos, 1); } else { $unused[] = $value; } } if (! empty($unused)) { $sql .= ' /* Unused values: "' . join('", "', $unused) . '" */'; } $pre = Html::tag('pre', $sql); CopyToClipboard::attachTo($pre); $this->content->add($pre); } return true; } // It only makes sense to export a single result to CSV or JSON $query = $queries[0]; // No matter the format, a limit should only apply if set if ($this->format !== null) { if (! Url::fromRequest()->hasParam('limit')) { $query->limit(null) ->offset(null); } } if ($this->format === 'json' || $this->format === 'csv') { $response = $this->getResponse(); $fileName = $this->view->title; ob_end_clean(); Environment::raiseExecutionTime(); if ($this->format === 'json') { $response ->setHeader('Content-Type', 'application/json') ->setHeader('Cache-Control', 'no-store') ->setHeader( 'Content-Disposition', 'attachment; filename=' . $fileName . '.json' ) ->sendResponse(); JsonResultSet::stream($query); } else { $response ->setHeader('Content-Type', 'text/csv') ->setHeader('Cache-Control', 'no-store') ->setHeader( 'Content-Disposition', 'attachment; filename=' . $fileName . '.csv' ) ->sendResponse(); CsvResultSet::stream($query); } } $this->getTabs()->enableDataExports(); } public function dispatch($action) { // Notify helpers of action preDispatch state $this->_helper->notifyPreDispatch(); $this->preDispatch(); if ($this->getRequest()->isDispatched()) { // If pre-dispatch hooks introduced a redirect then stop dispatch // @see ZF-7496 if (! $this->getResponse()->isRedirect()) { $interceptable = $this->$action(); if ($interceptable instanceof Generator) { foreach ($interceptable as $stopSignal) { if ($stopSignal === true) { $this->formatProcessed = true; break; } } } } $this->postDispatch(); } // whats actually important here is that this action controller is // shutting down, regardless of dispatching; notify the helpers of this // state $this->_helper->notifyPostDispatch(); } protected function addContent(ValidHtml $content) { if ($content instanceof StateItemTable) { $this->content->getAttributes()->add('class', 'full-height'); } return parent::addContent($content); } public function filter(Query $query, ?Filter\Rule $filter = null): self { if ($this->format !== 'sql' || $this->hasPermission('config/authentication/roles/show')) { $this->applyRestrictions($query); } if ($query instanceof UnionQuery) { foreach ($query->getUnions() as $query) { $query->filter($filter ?: $this->getFilter()); } } else { $query->filter($filter ?: $this->getFilter()); } return $this; } public function preDispatch() { parent::preDispatch(); $this->format = $this->params->shift( 'format', $this->getRequest()->isApiRequest() ? 'json' : null ); } public function postDispatch() { if (! $this->formatProcessed && $this->format !== null && $this->format !== 'pdf') { // The purpose of this is not only to show that a requested format isn't supported. // It's main purpose is to not allow to bypass restrictions with `?format=sql` as // it may be possible that an action applies restrictions, but doesn't support any // output formats. Since the restrictions are bypassed in method `$this->filter()` // for the SQL output format and the actual format processing is part of a different // method (`$this->export()`) which needs to be called explicitly by an action, // it's otherwise possible for bad individuals to access unrestricted data. $this->httpBadRequest(t('This route does not support the requested output format')); } parent::postDispatch(); } protected function moduleInit() { /** @var Web $app */ $app = Icinga::app(); $app->getFrontController() ->getPlugin('Zend_Controller_Plugin_ErrorHandler') ->setErrorHandlerModule('icingadb'); } } icingadb-web-1.4.0/library/Icingadb/Web/Navigation/000077500000000000000000000000001516124654200220155ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Web/Navigation/Action.php000066400000000000000000000054031516124654200237450ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web\Navigation; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Macros; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use Icinga\Web\Navigation\NavigationItem; use ipl\Web\Url; class Action extends NavigationItem { use Auth; use Macros; /** * Whether this action's macros were already resolved * * @var bool */ protected $resolved = false; /** * This action's object * * @var Host|Service */ protected $object; /** * The filter to use when being asked whether to render this action * * @var string */ protected $filter; /** * This action's raw url attribute * * @var string */ protected $rawUrl; /** * Set this action's object * * @param Host|Service $object * * @return $this */ public function setObject($object): self { $this->object = $object; return $this; } /** * Get this action's object * * @return Host|Service */ protected function getObject() { return $this->object; } /** * Set the filter to use when being asked whether to render this action * * @param string $filter * * @return $this */ public function setFilter(string $filter): self { $this->filter = $filter; return $this; } /** * Get the filter to use when being asked whether to render this action * * @return ?string */ public function getFilter(): ?string { return $this->filter; } /** * Set this item's url * * @param \Icinga\Web\Url|string $url * * @return $this */ public function setUrl($url): self { if (is_string($url)) { $this->rawUrl = $url; } else { parent::setUrl($url); } return $this; } public function getUrl(): ?\Icinga\Web\Url { $url = parent::getUrl(); if (! $this->resolved && $url === null && $this->rawUrl !== null && $this->object !== null) { $this->setUrl(Url::fromPath($this->expandMacros($this->rawUrl, $this->getObject()))); $this->resolved = true; return parent::getUrl(); } else { return $url; } } public function getRender(): bool { if ($this->render === null) { $filter = $this->getFilter(); $this->render = ! $filter || $this->isMatchedOn($filter, $this->getObject()); } return $this->render; } } icingadb-web-1.4.0/library/Icingadb/Web/Navigation/IcingadbHostAction.php000066400000000000000000000003241516124654200262210ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web\Navigation; class IcingadbHostAction extends Action { } icingadb-web-1.4.0/library/Icingadb/Web/Navigation/IcingadbServiceAction.php000066400000000000000000000003271516124654200267070ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web\Navigation; class IcingadbServiceAction extends Action { } icingadb-web-1.4.0/library/Icingadb/Web/Navigation/Renderer/000077500000000000000000000000001516124654200235635ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php000066400000000000000000000017371516124654200276500ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web\Navigation\Renderer; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\HoststateSummary; use ipl\Web\Url; class HostProblemsBadge extends ProblemsBadge { use Auth; protected function fetchProblemsCount() { $summary = HoststateSummary::on($this->getDb()); $this->applyRestrictions($summary); $count = (int) $summary->first()->hosts_down_unhandled; if ($count) { $this->setTitle(sprintf( tp('One unhandled host down', '%d unhandled hosts down', $count), $count )); } return $count; } protected function getUrl(): Url { return Links::hosts()->setParams(['host.state.is_problem' => 'y', 'sort' => 'host.state.severity desc']); } } icingadb-web-1.4.0/library/Icingadb/Web/Navigation/Renderer/ProblemsBadge.php000066400000000000000000000075721516124654200270150ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web\Navigation\Renderer; use Exception; use Icinga\Application\Logger; use Icinga\Module\Icingadb\Common\Database; use Icinga\Web\Navigation\NavigationItem; use Icinga\Web\Navigation\Renderer\NavigationItemRenderer; use ipl\Html\HtmlDocument; use ipl\Html\HtmlString; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBadge; abstract class ProblemsBadge extends NavigationItemRenderer { use Database; public const STATE_CRITICAL = 'critical'; public const STATE_UNKNOWN = 'unknown'; /** @var int Count cache */ protected $count; /** @var string State text */ protected $state; /** @var string Title */ protected $title; protected $linkDisabled; abstract protected function fetchProblemsCount(); abstract protected function getUrl(); public function getProblemsCount() { if ($this->count === null) { try { $count = $this->fetchProblemsCount(); } catch (Exception $e) { Logger::debug($e); $this->count = 1; $this->setState(static::STATE_UNKNOWN); $this->setTitle($e->getMessage()); return $this->count; } $this->count = $count; $this->setState(static::STATE_CRITICAL); } return $this->count; } /** * Set the state text * * @param string $state * * @return $this */ public function setState(string $state): self { $this->state = $state; return $this; } /** * Get the state text * * @return string */ public function getState(): string { if ($this->state === null) { throw new \LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->state; } /** * Set the title * * @param string $title * * @return $this */ public function setTitle(string $title): self { $this->title = $title; return $this; } /** * Get the title * * @return ?string */ public function getTitle() { return $this->title; } public function render(?NavigationItem $item = null): string { if ($item === null) { $item = $this->getItem(); } $item->setCssClass('badge-nav-item icinga-module module-icingadb'); $html = new HtmlDocument(); $badge = $this->createBadge(); if ($badge !== null) { if ($this->linkDisabled) { $badge->addAttributes(['class' => 'disabled']); $this->setEscapeLabel(false); $label = $this->view()->escape($item->getLabel()); $item->setLabel($badge . $label); } else { $html->add(new Link($badge, $this->getUrl(), ['title' => $this->getTitle()])); } } return $html ->prepend(new HtmlString(parent::render($item))) ->render(); } protected function createBadge() { $count = $this->getProblemsCount(); if ($count) { return (new StateBadge($this->round($count), $this->getState())) ->addAttributes(['class' => 'badge', 'title' => $this->getTitle()]); } return null; } protected function round($count) { if ($count > 1000000) { $count = round($count, -6) / 1000000 . 'M'; } elseif ($count > 1000) { $count = round($count, -3) / 1000 . 'k'; } return $count; } public function disableLink() { $this->linkDisabled = true; return $this; } } icingadb-web-1.4.0/library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php000066400000000000000000000020231516124654200303200ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web\Navigation\Renderer; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\ServicestateSummary; use ipl\Web\Url; class ServiceProblemsBadge extends ProblemsBadge { use Auth; protected function fetchProblemsCount() { $summary = ServicestateSummary::on($this->getDb()); $this->applyRestrictions($summary); $count = (int) $summary->first()->services_critical_unhandled; if ($count) { $this->setTitle(sprintf( tp('One unhandled service critical', '%d unhandled services critical', $count), $count )); } return $count; } protected function getUrl(): Url { return Links::services() ->setParams(['service.state.is_problem' => 'y', 'sort' => 'service.state.severity desc']); } } icingadb-web-1.4.0/library/Icingadb/Web/Navigation/Renderer/TotalProblemsBadge.php000066400000000000000000000034221516124654200300070ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Web\Navigation\Renderer; use Icinga\Web\Navigation\Renderer\BadgeNavigationItemRenderer; class TotalProblemsBadge extends BadgeNavigationItemRenderer { /** * Cached count * * @var int */ protected $count; /** * State to severity map * * @var array */ protected static $stateSeverityMap = [ self::STATE_OK => 0, self::STATE_PENDING => 1, self::STATE_UNKNOWN => 2, self::STATE_WARNING => 3, self::STATE_CRITICAL => 4, ]; /** * Severity to state map * * @var array */ protected static $severityStateMap = [ self::STATE_OK, self::STATE_PENDING, self::STATE_UNKNOWN, self::STATE_WARNING, self::STATE_CRITICAL ]; public function getCount() { if ($this->count === null) { $countMap = array_fill(0, 5, 0); $maxSeverity = 0; foreach ($this->getItem()->getChildren() as $child) { $renderer = $child->getRenderer(); if ($renderer instanceof ProblemsBadge) { $count = $renderer->getProblemsCount(); if ($count) { $severity = static::$stateSeverityMap[$renderer->getState()]; $countMap[$severity] += $count; $maxSeverity = max($maxSeverity, $severity); } } } $this->count = $countMap[$maxSeverity]; $this->state = static::$severityStateMap[$maxSeverity]; } return $this->count; } } icingadb-web-1.4.0/library/Icingadb/Widget/000077500000000000000000000000001516124654200204245ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Widget/CheckAttempt.php000066400000000000000000000027021516124654200235120ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use ipl\Html\BaseHtmlElement; use ipl\Html\FormattedString; use ipl\Web\Widget\Ball; /** * Visually represents the check attempts taken out of max check attempts. */ class CheckAttempt extends BaseHtmlElement { protected $tag = 'div'; protected $defaultAttributes = ['class' => 'check-attempt']; /** @var int Current attempt */ protected $attempt; /** @var int Max check attempts */ protected $maxAttempts; /** * Create a new check attempt widget * * @param int $attempt Current check attempt * @param int $maxAttempts Max check attempts */ public function __construct(int $attempt, int $maxAttempts) { $this->attempt = $attempt; $this->maxAttempts = $maxAttempts; } protected function assemble() { if ($this->attempt == $this->maxAttempts) { return; } if ($this->maxAttempts > 5) { $this->add(FormattedString::create('%d/%d', $this->attempt, $this->maxAttempts)); } else { for ($i = 0; $i < $this->attempt; ++$i) { $this->add(new Ball(Ball::SIZE_SMALL)); } for ($i = $this->attempt; $i < $this->maxAttempts; ++$i) { $this->add(new Ball(Ball::SIZE_TINY)); } } } } icingadb-web-1.4.0/library/Icingadb/Widget/DependencyNodeStateBadges.php000066400000000000000000000015271516124654200261350ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Module\Icingadb\Common\StateBadges; /** * State badges for the dependency nodes */ class DependencyNodeStateBadges extends StateBadges { protected function getType(): string { return 'nodes'; } protected function getPrefix(): string { return 'nodes'; } protected function assemble(): void { $this->addAttributes(['class' => 'dependency-node-state-badges']); $this->add(array_filter([ $this->createGroup('problem'), $this->createGroup('warning'), $this->createGroup('unknown'), $this->createBadge('ok'), $this->createBadge('pending') ])); } } icingadb-web-1.4.0/library/Icingadb/Widget/DependencyNodeStatistics.php000066400000000000000000000032761516124654200261040ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Chart\Donut; use Icinga\Module\Icingadb\Widget\Detail\ObjectStatistics; use ipl\Html\Text; use ipl\Html\ValidHtml; use ipl\Html\HtmlString; /** * Dependency node statistics */ class DependencyNodeStatistics extends ObjectStatistics { protected $summary; public function __construct($summary) { $this->summary = $summary; } protected function createDonut(): ValidHtml { $donut = (new Donut()) ->addSlice($this->summary->nodes_ok, ['class' => 'slice-state-ok']) ->addSlice($this->summary->nodes_warning_handled, ['class' => 'slice-state-warning-handled']) ->addSlice($this->summary->nodes_warning_unhandled, ['class' => 'slice-state-warning']) ->addSlice($this->summary->nodes_problem_handled, ['class' => 'slice-state-critical-handled']) ->addSlice($this->summary->nodes_problem_unhandled, ['class' => 'slice-state-critical']) ->addSlice($this->summary->nodes_unknown_handled, ['class' => 'slice-state-unknown-handled']) ->addSlice($this->summary->nodes_unknown_unhandled, ['class' => 'slice-state-unknown']) ->addSlice($this->summary->nodes_pending, ['class' => 'slice-state-pending']); return HtmlString::create($donut->render()); } protected function createTotal(): ValidHtml { return Text::create($this->shortenAmount($this->summary->nodes_total)); } protected function createBadges(): ValidHtml { return new DependencyNodeStateBadges($this->summary); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/000077500000000000000000000000001516124654200216265ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Widget/Detail/CheckStatistics.php000066400000000000000000000340611516124654200254330ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Date\DateFormatter; use Icinga\Module\Icingadb\Widget\CheckAttempt; use Icinga\Util\Format; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\FormattedString; use ipl\Html\HtmlElement; use ipl\Html\HtmlString; use ipl\Html\Text; use ipl\Web\Common\Card; use ipl\Web\Widget\EmptyState; use ipl\Web\Compat\StyleWithNonce; use ipl\Web\Widget\TimeAgo; use ipl\Web\Widget\TimeSince; use ipl\Web\Widget\TimeUntil; use ipl\Web\Widget\VerticalKeyValue; class CheckStatistics extends Card { public const TOP_LEFT_BUBBLE_FLAG = <<<'SVG' SVG; public const TOP_RIGHT_BUBBLE_FLAG = <<<'SVG' SVG; protected $object; protected $tag = 'div'; protected $defaultAttributes = ['class' => ['progress-bar', 'check-statistics']]; public function __construct($object) { $this->object = $object; } protected function assembleBody(BaseHtmlElement $body) { $styleElement = (new StyleWithNonce()) ->setModule('icingadb'); $hPadding = 10; $durationScale = 80; $checkInterval = $this->getCheckInterval(); $timeline = new HtmlElement('div', Attributes::create(['class' => ['check-timeline', 'timeline']])); $above = new HtmlElement('ul', Attributes::create(['class' => 'above'])); $below = new HtmlElement('ul', Attributes::create(['class' => 'below'])); $progressBar = new HtmlElement('div', Attributes::create(['class' => 'bar'])); $overdueBar = null; $now = time(); $executionTime = ($this->object->state->execution_time / 1000) + ($this->object->state->latency / 1000); $nextCheckTime = $this->object->state->next_check !== null && ! $this->isChecksDisabled() ? $this->object->state->next_check->getTimestamp() : null; if ($this->object->state->is_overdue) { $nextCheckTime = $this->object->state->next_update->getTimestamp(); $durationScale = 60; $overdueBar = new HtmlElement( 'div', Attributes::create(['class' => 'timeline-overlay']), new HtmlElement('div', Attributes::create(['class' => 'now'])) ); $above->addHtml(new HtmlElement( 'li', Attributes::create(['class' => 'now']), new HtmlElement( 'div', Attributes::create(['class' => 'bubble']), new HtmlElement('strong', null, Text::create(t('Now'))) ) )); $this->getAttributes()->add('class', 'check-overdue'); } else { $progressBar->addHtml(new HtmlElement('div', Attributes::create(['class' => 'now']))); } if ($nextCheckTime !== null && ! $this->object->state->is_overdue && $nextCheckTime < $now) { // If the next check is already in the past but not overdue, it means the check is probably running. // Icinga only updates the state once the check reports a result, that's why we have to simulate the // execution start and end time, as well as the next check time. $lastUpdateTime = $nextCheckTime; $nextCheckTime = $this->object->state->next_update->getTimestamp() - $executionTime; $executionEndTime = $lastUpdateTime + $executionTime; } else { $lastUpdateTime = $this->object->state->last_update !== null ? $this->object->state->last_update->getTimestamp() - $executionTime : null; $executionEndTime = $this->object->state->last_update !== null ? $this->object->state->last_update->getTimestamp() : null; } if ($this->object->state->is_overdue) { $leftNow = 100; } elseif ($nextCheckTime === null) { $leftNow = 0; } elseif (! $this->object->state->is_reachable && time() - $executionEndTime > $checkInterval * 2) { // We have no way of knowing whether the dependency pauses check scheduling. // The only way to detect this, is to measure how old the last update is. $nextCheckTime = null; $leftNow = 0; } elseif ($nextCheckTime - $lastUpdateTime <= 0) { $leftNow = 0; } else { $leftNow = 100 * (1 - ($nextCheckTime - time()) / ($nextCheckTime - $lastUpdateTime)); if ($leftNow > 100) { $leftNow = 100; } elseif ($leftNow < 0) { $leftNow = 0; } } $styleElement->addFor($progressBar, ['width' => sprintf('%F%%', $leftNow)]); $leftExecutionEnd = $nextCheckTime !== null && $nextCheckTime - $lastUpdateTime > 0 ? $durationScale * ( 1 - ($nextCheckTime - $executionEndTime) / ($nextCheckTime - $lastUpdateTime) ) : 0; $markerLast = new HtmlElement('div', Attributes::create([ 'class' => ['highlighted', 'marker', 'left'], 'title' => $lastUpdateTime !== null ? DateFormatter::formatDateTime($lastUpdateTime) : null ])); $markerNext = new HtmlElement('div', Attributes::create([ 'class' => ['highlighted', 'marker', 'right'], 'title' => $nextCheckTime !== null ? DateFormatter::formatDateTime($nextCheckTime) : null ])); $markerExecutionEnd = new HtmlElement('div', Attributes::create(['class' => ['highlighted', 'marker']])); $styleElement->addFor($markerExecutionEnd, [ 'left' => sprintf('%F%%', $hPadding + $leftExecutionEnd) ]); $progress = new HtmlElement('div', Attributes::create([ 'class' => ['progress', time() < $executionEndTime ? 'running' : null] ]), $progressBar); if ($nextCheckTime !== null) { $progress->addAttributes([ 'data-animate-progress' => true, 'data-start-time' => $lastUpdateTime, 'data-end-time' => $nextCheckTime, 'data-switch-after' => $executionTime, 'data-switch-class' => 'running' ]); } $timeline->addHtml( $progress, $markerLast, $markerExecutionEnd, $markerNext )->add($overdueBar); $executionStart = new HtmlElement( 'li', Attributes::create(['class' => 'left']), new HtmlElement( 'div', Attributes::create(['class' => ['bubble', 'upwards', 'top-right-aligned']]), new VerticalKeyValue( t('Execution Start'), $lastUpdateTime ? new TimeAgo($lastUpdateTime) : t('PENDING') ), HtmlString::create(self::TOP_RIGHT_BUBBLE_FLAG) ) ); $executionEnd = new HtmlElement( 'li', Attributes::create(['class' => 'positioned']), new HtmlElement( 'div', Attributes::create(['class' => ['bubble', 'upwards', 'top-left-aligned']]), new VerticalKeyValue( t('Execution End'), $executionEndTime !== null ? ($executionEndTime > $now ? new TimeUntil($executionEndTime) : new TimeAgo($executionEndTime)) : t('PENDING') ), HtmlString::create(self::TOP_LEFT_BUBBLE_FLAG) ) ); $styleElement->addFor($executionEnd, ['left' => sprintf('%F%%', $hPadding + $leftExecutionEnd)]); $intervalLine = new HtmlElement( 'li', Attributes::create(['class' => 'interval-line']), new VerticalKeyValue(t('Interval'), Format::seconds($checkInterval)) ); $styleElement->addFor($intervalLine, [ 'left' => sprintf('%F%%', $hPadding + $leftExecutionEnd), 'width' => sprintf('%F%%', $durationScale - $leftExecutionEnd) ]); $executionLine = new HtmlElement( 'li', Attributes::create(['class' => ['interval-line', 'execution-line']]), new VerticalKeyValue( sprintf('%s / %s', t('Execution Time'), t('Latency')), FormattedString::create( '%s / %s', $this->object->state->execution_time !== null ? Format::seconds($this->object->state->execution_time / 1000) : (new EmptyState(t('n. a.')))->setTag('span'), $this->object->state->latency !== null ? Format::seconds($this->object->state->latency / 1000) : (new EmptyState(t('n. a.')))->setTag('span') ) ) ); $styleElement->addFor($executionLine, [ 'left' => sprintf('%F%%', $hPadding), 'width' => sprintf('%F%%', $leftExecutionEnd) ]); if ($executionEndTime !== null) { $executionLine->addHtml(new HtmlElement('div', Attributes::create(['class' => 'start']))); $executionLine->addHtml(new HtmlElement('div', Attributes::create(['class' => 'end']))); } if ($this->isChecksDisabled()) { $nextCheckBubbleContent = new VerticalKeyValue( t('Next Check'), t('n.a') ); $this->addAttributes(['class' => 'checks-disabled']); } else { $nextCheckBubbleContent = $this->object->state->is_overdue ? new VerticalKeyValue(t('Overdue'), new TimeSince($nextCheckTime)) : new VerticalKeyValue( t('Next Check'), $nextCheckTime !== null ? ($nextCheckTime > $now ? new TimeUntil($nextCheckTime) : new TimeAgo($nextCheckTime)) : t('PENDING') ); } $nextCheck = new HtmlElement( 'li', Attributes::create(['class' => 'right']), new HtmlElement( 'div', Attributes::create(['class' => ['bubble', 'upwards']]), $nextCheckBubbleContent ) ); $above->addHtml($executionLine); $below->addHtml( $executionStart, $executionEnd, $intervalLine, $nextCheck ); $body->addHtml($above, $timeline, $below, $styleElement); } /** * Checks if both active and passive checks are disabled * * @return bool */ protected function isChecksDisabled(): bool { return ! ($this->object->active_checks_enabled || $this->object->passive_checks_enabled); } protected function assembleHeader(BaseHtmlElement $header) { $checkSource = (new EmptyState(t('n. a.')))->setTag('span'); if ($this->object->state->check_source) { $checkSource = Text::create($this->object->state->check_source); } $header->addHtml( new VerticalKeyValue(t('Command'), $this->object->checkcommand_name), new VerticalKeyValue( t('Scheduling Source'), $this->object->state->scheduling_source ?? (new EmptyState(t('n. a.')))->setTag('span') ) ); if ($this->object->timeperiod->id) { $header->addHtml(new VerticalKeyValue( t('Timeperiod'), $this->object->timeperiod->display_name ?? $this->object->timeperiod->name )); } $header->addHtml( new VerticalKeyValue( t('Attempts'), new CheckAttempt((int) $this->object->state->check_attempt, (int) $this->object->max_check_attempts) ), new VerticalKeyValue(t('Check Source'), $checkSource) ); } /** * Get the active `check_interval` OR `check_retry_interval` * * @return int */ protected function getCheckInterval(): int { if (! ($this->object->state->is_problem && $this->object->state->state_type === 'soft')) { return $this->object->check_interval; } $delay = ($this->object->state->execution_time + $this->object->state->latency / 1000) + 5; $interval = $this->object->state->next_check->getTimestamp() - $this->object->state->last_update->getTimestamp(); // In case passive check is used, the check_retry_interval has no effect. // Since there is no flag in the database to check if the passive check was triggered. // We have to manually check if the check_retry_interval matches the calculated interval. if ( $this->object->check_retry_interval - $delay <= $interval && $this->object->check_retry_interval + $delay >= $interval ) { return $this->object->check_retry_interval; } return $this->object->check_interval; } protected function assemble() { parent::assemble(); if ($this->isChecksDisabled()) { $this->addHtml(new HtmlElement( 'div', Attributes::create(['class' => 'checks-disabled-overlay']), new HtmlElement( 'strong', Attributes::create(['class' => 'notes']), Text::create(t('active and passive checks are disabled')) ) )); } } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/CommentDetail.php000066400000000000000000000107201516124654200250640ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Date\DateFormatter; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\TicketLinks; use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Widget\MarkdownText; use Icinga\Module\Icingadb\Forms\Command\Object\DeleteCommentForm; use ipl\Stdlib\Filter; use ipl\Web\Filter\QueryString; use ipl\Web\Widget\HorizontalKeyValue; use ipl\Web\Widget\StateBall; use ipl\Web\Widget\TimeUntil; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; class CommentDetail extends BaseHtmlElement { use Auth; use TicketLinks; protected $comment; protected $defaultAttributes = ['class' => ['object-detail', 'comment-detail']]; protected $tag = 'div'; public function __construct(Comment $comment) { $this->comment = $comment; } protected function createComment(): array { return [ Html::tag('h2', t('Comment')), new MarkdownText($this->createTicketLinks($this->comment->text)) ]; } protected function createDetails(): array { $details = []; if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { if ($this->comment->object_type === 'host') { $details[] = new HorizontalKeyValue(t('Host'), [ $this->comment->host->name, ' ', new StateBall($this->comment->host->state->getStateText()) ]); } else { $details[] = new HorizontalKeyValue(t('Service'), Html::sprintf( t('%s on %s', ' on '), [$this->comment->service->name, ' ', new StateBall($this->comment->service->state->getStateText())], $this->comment->host->name )); } $details[] = new HorizontalKeyValue(t('Author'), $this->comment->author); $details[] = new HorizontalKeyValue( t('Acknowledgement'), $this->comment->entry_type === 'ack' ? t('Yes') : t('No') ); $details[] = new HorizontalKeyValue( t('Persistent'), $this->comment->is_persistent ? t('Yes') : t('No') ); $details[] = new HorizontalKeyValue( t('Created'), DateFormatter::formatDateTime($this->comment->entry_time->getTimestamp()) ); $details[] = new HorizontalKeyValue(t('Expires'), $this->comment->expire_time !== null ? DateFormatter::formatDateTime($this->comment->expire_time->getTimestamp()) : t('Never')); } else { if ($this->comment->expire_time !== null) { $details[] = Html::tag( 'p', Html::sprintf( $this->comment->entry_type === 'ack' ? t('This acknowledgement expires %s.', '..') : t('This comment expires %s.', '..'), new TimeUntil($this->comment->expire_time->getTimestamp()) ) ); } if ($this->comment->is_sticky) { $details[] = Html::tag('p', t('This acknowledgement is sticky.')); } } if (! empty($details)) { array_unshift($details, Html::tag('h2', t('Details'))); } return $details; } protected function createRemoveCommentForm() { if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { return null; } $action = Links::commentsDelete(); $action->setFilter(Filter::equal('comment.name', $this->comment->name)); return (new DeleteCommentForm()) ->setObjects([$this->comment]) ->setAction($action->getAbsoluteUrl()); } protected function assemble() { $this->add($this->createComment()); $details = $this->createDetails(); if (! empty($details)) { $this->add($details); } if ( $this->isGrantedOn( 'icingadb/command/comment/delete', $this->comment->{$this->comment->object_type} ) ) { $this->add($this->createRemoveCommentForm()); } } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/CustomVarTable.php000066400000000000000000000151751516124654200252430ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Hook\CustomVarRendererHook; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Orm\Model; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\Icon; use Closure; class CustomVarTable extends BaseHtmlElement { /** @var array The variables */ protected $data; /** @var ?Model The object the variables are bound to */ protected $object; /** @var Closure Callback to apply hooks */ protected $hookApplier; /** @var array The groups as identified by hooks */ protected $groups = []; /** @var string Header title */ protected $headerTitle; /** @var int The nesting level */ protected $level = 0; protected $tag = 'table'; /** @var HtmlElement The table body */ protected $body; protected $defaultAttributes = [ 'class' => ['custom-var-table', 'name-value-table'] ]; /** * Create a new CustomVarTable * * @param iterable $data * @param ?Model $object */ public function __construct($data, ?Model $object = null) { $this->data = $data; $this->object = $object; $this->body = new HtmlElement('tbody'); } /** * Set the header to show * * @param string $title * * @return $this */ protected function setHeader(string $title): self { $this->headerTitle = $title; return $this; } /** * Add a new row to the body * * @param mixed $name * @param mixed $value * * @return void */ protected function addRow($name, $value) { $this->body->addHtml(new HtmlElement( 'tr', Attributes::create(['class' => "level-{$this->level}"]), new HtmlElement('th', null, Html::wantHtml($name)), new HtmlElement('td', null, Html::wantHtml($value)) )); } /** * Render a variable * * @param mixed $name * @param mixed $value * * @return void */ protected function renderVar($name, $value) { if ($this->object !== null && $this->level === 0) { list($name, $value, $group) = call_user_func($this->hookApplier, $name, $value); if ($group !== null) { $this->groups[$group][] = [$name, $value]; return; } } $isArray = is_array($value); switch (true) { case $isArray && is_int(key($value)): $this->renderArray($name, $value); break; case $isArray: $this->renderObject($name, $value); break; default: $this->renderScalar($name, $value); } } /** * Render an array * * @param string $name * @param array $array * * @return void */ protected function renderArray($name, array $array) { $numItems = count($array); $name = (new HtmlDocument())->addHtml( Html::wantHtml($name), Text::create(' (Array)') ); $this->addRow($name, sprintf(tp('%d item', '%d items', $numItems), $numItems)); ++$this->level; ksort($array); foreach ($array as $key => $value) { $this->renderVar("[$key]", $value); } --$this->level; } /** * Render an object (associative array) * * @param mixed $name * @param array $object * * @return void */ protected function renderObject($name, array $object) { $numItems = count($object); $this->addRow($name, sprintf(tp('%d item', '%d items', $numItems), $numItems)); ++$this->level; ksort($object); foreach ($object as $key => $value) { $this->renderVar($key, $value); } --$this->level; } /** * Render a scalar * * @param mixed $name * @param mixed $value * * @return void */ protected function renderScalar($name, $value) { if ($value === '') { $value = new EmptyState(t('empty string')); } $this->addRow($name, $value); } /** * Render a group * * @param string $name * @param iterable $entries * * @return void */ protected function renderGroup(string $name, $entries) { $table = new self($entries); $wrapper = $this->getWrapper(); if ($wrapper === null) { $wrapper = new HtmlDocument(); $wrapper->addHtml($this); $this->prependWrapper($wrapper); } $wrapper->addHtml($table->setHeader($name)); } protected function assemble() { if ($this->object !== null) { $this->hookApplier = CustomVarRendererHook::prepareForObject($this->object); } if ($this->headerTitle !== null) { $this->getAttributes() ->add('class', 'collapsible') ->add('data-visible-height', 0) ->add('data-state-collapses', true) ->add('data-toggle-element', 'thead') ->add( 'id', preg_replace('/\s+/', '-', strtolower($this->headerTitle)) . '-customvars' ); $this->addHtml(new HtmlElement('thead', null, new HtmlElement( 'tr', null, new HtmlElement( 'th', Attributes::create(['colspan' => 2]), new HtmlElement( 'span', null, new Icon('angle-right'), new Icon('angle-down') ), Text::create($this->headerTitle) ) ))); } if (is_array($this->data)) { ksort($this->data); } foreach ($this->data as $name => $value) { $this->renderVar($name, $value); } $this->addHtml($this->body); // Hooks can return objects as replacement for keys, hence a generator is needed for group entries $genGenerator = function ($entries) { foreach ($entries as list($key, $value)) { yield $key => $value; } }; foreach ($this->groups as $group => $entries) { $this->renderGroup($group, $genGenerator($entries)); } } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/DowntimeCard.php000066400000000000000000000221141516124654200247170ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Model\Downtime; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Web\Compat\StyleWithNonce; use ipl\Web\Widget\TimeAgo; use ipl\Web\Widget\TimeUntil; use ipl\Web\Widget\VerticalKeyValue; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; class DowntimeCard extends BaseHtmlElement { protected $downtime; protected $duration; protected $defaultAttributes = ['class' => 'progress-bar downtime-progress']; protected $tag = 'div'; protected $start; protected $end; public function __construct(Downtime $downtime) { $this->downtime = $downtime; $this->start = $this->downtime->scheduled_start_time->getTimestamp(); $this->end = $this->downtime->scheduled_end_time->getTimestamp(); if ($this->downtime->end_time && $this->downtime->end_time > $this->downtime->scheduled_end_time) { $this->duration = $this->downtime->end_time->getTimestamp() - $this->start; } else { $this->duration = $this->end - $this->start; } } protected function assemble() { $styleElement = (new StyleWithNonce()) ->setModule('icingadb'); $timeline = Html::tag('div', ['class' => 'downtime-timeline timeline']); $hPadding = 10; $above = Html::tag('ul', ['class' => 'above']); $below = Html::tag('ul', ['class' => 'below']); $markerStart = new HtmlElement('div', Attributes::create(['class' => ['marker' , 'left']])); $markerEnd = new HtmlElement('div', Attributes::create(['class' => ['marker', 'right']])); $timelineProgress = null; $flexProgress = null; $markerFlexStart = null; $markerFlexEnd = null; if ($this->end < time()) { $endTime = new TimeAgo($this->end); } else { $endTime = new TimeUntil($this->end); } if ($this->downtime->is_flexible && $this->downtime->is_in_effect) { $this->addAttributes(['class' => 'flexible in-effect']); $flexStartLeft = $hPadding + $this->calcRelativeLeft($this->downtime->start_time->getTimestamp()); $flexEndLeft = $hPadding + $this->calcRelativeLeft($this->downtime->end_time->getTimestamp()); $evade = false; if ($flexEndLeft - $flexStartLeft < 2) { $flexStartLeft -= 1; $flexEndLeft += 1; if ($flexEndLeft > $hPadding + $this->calcRelativeLeft($this->end)) { $flexEndLeft = $hPadding + $this->calcRelativeLeft($this->end) - .5; $flexStartLeft = $flexEndLeft - 2; } if ($flexStartLeft < $hPadding + $this->calcRelativeLeft($this->start)) { $flexStartLeft = $hPadding + $this->calcRelativeLeft($this->start) + .5; $flexEndLeft = $flexStartLeft + 2; } $evade = true; } $markerFlexStart = new HtmlElement('div', Attributes::create(['class' => ['highlighted', 'marker']])); $markerFlexEnd = new HtmlElement('div', Attributes::create(['class' => ['highlighted', 'marker']])); $styleElement ->addFor($markerFlexStart, ['left' => sprintf('%F%%', $flexStartLeft)]) ->addFor($markerFlexEnd, ['left' => sprintf('%F%%', $flexEndLeft)]); $scheduledEndBubble = new HtmlElement( 'li', new Attributes(['class' => 'right']), new HtmlElement( 'div', Attributes::create(['class' => ['bubble', 'upwards']]), new VerticalKeyValue(t('Scheduled End'), $endTime) ) ); $timelineProgress = new HtmlElement('div', Attributes::create([ 'class' => ['progress', 'downtime-elapsed'], 'data-animate-progress' => true, 'data-start-time' => ((float) $this->downtime->start_time->format('U.u')), 'data-end-time' => ((float) $this->downtime->end_time->format('U.u')) ]), new HtmlElement( 'div', Attributes::create(['class' => 'bar']), new HtmlElement('div', Attributes::create(['class' => 'now'])) )); $styleElement->addFor($timelineProgress, [ 'left' => sprintf('%F%%', $flexStartLeft), 'width' => sprintf('%F%%', $flexEndLeft - $flexStartLeft) ]); $styleElement ->addFor($markerEnd, [ 'left' => sprintf('%F%%', $hPadding + $this->calcRelativeLeft($this->end)) ]) ->addFor($scheduledEndBubble, [ 'right' => sprintf( '%F%%', 100 - ($hPadding + $this->calcRelativeLeft($this->end)) ) ]); $below->add([ Html::tag( 'li', ['class' => 'left'], Html::tag( 'div', ['class' => ['bubble', 'upwards']], new VerticalKeyValue(t('Scheduled Start'), new TimeAgo($this->start)) ) ), $scheduledEndBubble ]); $aboveStart = Html::tag('li', ['class' => 'positioned'], Html::tag( 'div', ['class' => ['bubble', ($evade ? 'left-aligned' : null)]], new VerticalKeyValue(t('Start'), new TimeAgo($this->downtime->start_time->getTimestamp())) )); $aboveEnd = Html::tag('li', ['class' => 'positioned'], Html::tag( 'div', ['class' => ['bubble', ($evade ? 'right-aligned' : null)]], new VerticalKeyValue(t('End'), new TimeUntil($this->downtime->end_time->getTimestamp())) )); $styleElement ->addFor($aboveStart, ['right' => sprintf('%F%%', 100 - $flexStartLeft)]) ->addFor($aboveEnd, ['right' => sprintf('%F%%', 100 - $flexEndLeft)]); $above->add([$aboveStart, $aboveEnd, $styleElement]); } elseif ($this->downtime->is_flexible) { $this->addAttributes(['class' => 'flexible']); $below->add([ Html::tag( 'li', ['class' => 'left'], Html::tag( 'div', ['class' => ['bubble', 'upwards']], new VerticalKeyValue( t('Scheduled Start'), time() > $this->start ? new TimeAgo($this->start) : new TimeUntil($this->start) ) ) ), Html::tag( 'li', ['class' => 'right'], Html::tag( 'div', ['class' => ['bubble', 'upwards']], new VerticalKeyValue(t('Scheduled End'), $endTime) ) ) ]); $above = null; } else { if (time() >= $this->start) { $timelineProgress = new HtmlElement('div', Attributes::create([ 'class' => ['progress', 'downtime-elapsed'], 'data-animate-progress' => true, 'data-start-time' => $this->start, 'data-end-time' => $this->end ]), new HtmlElement( 'div', Attributes::create(['class' => 'bar']), new HtmlElement('div', Attributes::create(['class' => 'now'])) )); } $below->add([ Html::tag( 'li', ['class' => 'left'], Html::tag( 'div', ['class' => 'bubble upwards'], new VerticalKeyValue(t('Start'), new TimeAgo($this->start)) ) ), Html::tag( 'li', ['class' => 'right'], Html::tag( 'div', ['class' => 'bubble upwards'], new VerticalKeyValue(t('End'), new TimeUntil($this->end)) ) ) ]); $above = null; } $timeline->add([ $timelineProgress, $flexProgress, $markerStart, $markerEnd, $markerFlexStart, $markerFlexEnd ]); $this->add([ $above, $timeline, $below ]); } protected function calcRelativeLeft($value) { return round(($value - $this->start) / $this->duration * 80, 2); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/DowntimeDetail.php000066400000000000000000000166331516124654200252610ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Date\DateFormatter; use Icinga\Date\DateFormatter as WebDateFormatter; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Common\HostLink; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Widget\MarkdownText; use Icinga\Module\Icingadb\Common\ServiceLink; use Icinga\Module\Icingadb\Forms\Command\Object\DeleteDowntimeForm; use Icinga\Module\Icingadb\Model\Downtime; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Web\Url; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\HorizontalKeyValue; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Html\HtmlElement; use ipl\Html\TemplateString; use ipl\Html\Text; use ipl\Stdlib\Filter; use ipl\Web\Filter\QueryString; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBall; class DowntimeDetail extends BaseHtmlElement { use Auth; use Database; use HostLink; use ServiceLink; /** @var BaseHtmlElement */ protected $control; /** @var Downtime */ protected $downtime; protected $defaultAttributes = ['class' => ['object-detail', 'downtime-detail']]; protected $tag = 'div'; public function __construct(Downtime $downtime) { $this->downtime = $downtime; } protected function createCancelDowntimeForm() { $action = Links::downtimesDelete(); $action->setFilter(Filter::equal('downtime.name', $this->downtime->name)); return (new DeleteDowntimeForm()) ->setObjects([$this->downtime]) ->setAction($action->getAbsoluteUrl()); } protected function createTimeline(): DowntimeCard { return new DowntimeCard($this->downtime); } protected function assemble() { $this->add(Html::tag('h2', t('Comment'))); $this->add(Html::tag('div', [ new Icon('user'), Html::sprintf( t('%s commented: %s', ' ..: '), $this->downtime->author, new MarkdownText($this->downtime->comment) ) ])); $this->add(Html::tag('h2', t('Details'))); if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { $this->addHtml(new HorizontalKeyValue( t('Type'), $this->downtime->is_flexible ? t('Flexible') : t('Fixed') )); if ($this->downtime->object_type === 'host') { $this->addHtml(new HorizontalKeyValue(t('Host'), [ $this->downtime->host->name, ' ', new StateBall($this->downtime->host->state->getStateText()) ])); } else { $this->addHtml(new HorizontalKeyValue(t('Service'), Html::sprintf( t('%s on %s', ' on '), [ $this->downtime->service->name, ' ', new StateBall($this->downtime->service->state->getStateText()) ], $this->downtime->host->name ))); } } if ($this->downtime->triggered_by_id !== null || $this->downtime->parent_id !== null) { if ($this->downtime->triggered_by_id !== null) { $label = t('Triggered By'); $relatedDowntime = $this->downtime->triggered_by; } else { $label = t('Parent'); $relatedDowntime = $this->downtime->parent; } $this->addHtml(new HorizontalKeyValue( $label, HtmlElement::create('span', ['class' => 'accompanying-text'], TemplateString::create( $relatedDowntime->is_flexible ? t('{{#link}}Flexible Downtime{{/link}} for %s') : t('{{#link}}Fixed Downtime{{/link}} for %s'), ['link' => new Link(null, Links::downtime($relatedDowntime), ['class' => 'subject'])], ($relatedDowntime->object_type === 'host' ? $this->createHostLink($relatedDowntime->host, true) : $this->createServiceLink($relatedDowntime->service, $relatedDowntime->host, true)) )) )); } $this->add(new HorizontalKeyValue( t('Created'), WebDateFormatter::formatDateTime($this->downtime->entry_time->getTimestamp()) )); $this->add(new HorizontalKeyValue( t('Start time'), $this->downtime->start_time ? WebDateFormatter::formatDateTime($this->downtime->start_time->getTimestamp()) : new EmptyState(t('Not started yet')) )); $this->add(new HorizontalKeyValue( t('End time'), $this->downtime->end_time ? WebDateFormatter::formatDateTime($this->downtime->end_time->getTimestamp()) : new EmptyState(t('Not started yet')) )); $this->add(new HorizontalKeyValue( t('Scheduled Start'), WebDateFormatter::formatDateTime($this->downtime->scheduled_start_time->getTimestamp()) )); $this->add(new HorizontalKeyValue( t('Scheduled End'), WebDateFormatter::formatDateTime($this->downtime->scheduled_end_time->getTimestamp()) )); $this->add(new HorizontalKeyValue( t('Scheduled Duration'), DateFormatter::formatDuration($this->downtime->scheduled_duration / 1000) )); if ($this->downtime->is_flexible) { $this->add(new HorizontalKeyValue( t('Flexible Duration'), DateFormatter::formatDuration($this->downtime->flexible_duration / 1000) )); } $query = Downtime::on($this->getDb())->with([ 'host', 'host.state', 'service', 'service.host', 'service.host.state', 'service.state' ]) ->limit(3) ->filter(Filter::equal('parent_id', $this->downtime->id)) ->orFilter(Filter::equal('triggered_by_id', $this->downtime->id)); $this->applyRestrictions($query); $children = $query->peekAhead()->execute(); if ($children->hasResult()) { $this->addHtml( new HtmlElement('h2', null, Text::create(t('Children'))), new ObjectList($children), (new ShowMore($children, Links::downtimes()->setQueryString( QueryString::render(Filter::any( Filter::equal('downtime.parent.name', $this->downtime->name), Filter::equal('downtime.triggered_by.name', $this->downtime->name) )) )))->setBaseTarget('_next') ); } $this->add(Html::tag('h2', t('Progress'))); $this->add($this->createTimeline()); if ( getenv('ICINGAWEB_EXPORT_FORMAT') !== 'pdf' && $this->isGrantedOn( 'icingadb/command/downtime/delete', $this->downtime->{$this->downtime->object_type} ) ) { $this->add($this->createCancelDowntimeForm()); } } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/EventDetail.php000066400000000000000000000722421516124654200245520ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use DateTime; use DateTimeZone; use Icinga\Application\Config; use Icinga\Date\DateFormatter; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Common\HostLink; use Icinga\Module\Icingadb\Common\HostStates; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\TicketLinks; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Widget\MarkdownText; use Icinga\Module\Icingadb\Common\ServiceLink; use Icinga\Module\Icingadb\Common\ServiceStates; use Icinga\Module\Icingadb\Model\AcknowledgementHistory; use Icinga\Module\Icingadb\Model\CommentHistory; use Icinga\Module\Icingadb\Model\DowntimeHistory; use Icinga\Module\Icingadb\Model\FlappingHistory; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Model\NotificationHistory; use Icinga\Module\Icingadb\Model\StateHistory; use Icinga\Module\Icingadb\Util\PluginOutput; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Web\Url; use ipl\Web\Widget\CopyToClipboard; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\HorizontalKeyValue; use Icinga\Module\Icingadb\Widget\PluginOutputContainer; use ipl\Html\BaseHtmlElement; use ipl\Html\FormattedString; use ipl\Html\HtmlElement; use ipl\Html\TemplateString; use ipl\Html\Text; use ipl\Orm\ResultSet; use ipl\Stdlib\Filter; use ipl\Stdlib\Str; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; use ipl\Web\Widget\StateBall; class EventDetail extends BaseHtmlElement { use Auth; use Database; use HostLink; use ServiceLink; use TicketLinks; protected $tag = 'div'; protected $defaultAttributes = ['class' => 'object-detail']; /** @var History */ protected $event; public function __construct(History $event) { $this->event = $event; } protected function assembleNotificationEvent(NotificationHistory $notification) { $pluginOutput = []; $commandName = $notification->object_type === 'host' ? $this->event->host->checkcommand_name : $this->event->service->checkcommand_name; if (isset($commandName)) { if (empty($notification->text)) { $notificationText = new EmptyState(t('Output unavailable.')); } else { $notificationText = new PluginOutputContainer( (new PluginOutput($notification->text)) ->setCommandName($notification->object_type === 'host' ? $this->event->host->checkcommand_name : $this->event->service->checkcommand_name) ->setCharacterLimit( (int) Config::module('icingadb') ->get('settings', 'plugin_output_character_limit', 10000) ) ); CopyToClipboard::attachTo($notificationText); } $pluginOutput = [ HtmlElement::create('h2', null, $notification->author ? t('Comment') : t('Plugin Output')), HtmlElement::create('div', [ 'id' => 'check-output-' . $commandName, 'class' => 'collapsible', 'data-visible-height' => 100 ], $notificationText) ]; } else { $pluginOutput[] = new EmptyState(t('Waiting for Icinga DB to synchronize the config.')); } if ($notification->object_type === 'host') { $objectKey = t('Host'); $objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [ HtmlElement::create('span', ['class' => 'state-change'], [ new StateBall(HostStates::text($notification->previous_hard_state), StateBall::SIZE_MEDIUM), new StateBall(HostStates::text($notification->state), StateBall::SIZE_MEDIUM) ]), ' ', HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) ]); } else { $objectKey = t('Service'); $objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [ HtmlElement::create('span', ['class' => 'state-change'], [ new StateBall(ServiceStates::text($notification->previous_hard_state), StateBall::SIZE_MEDIUM), new StateBall(ServiceStates::text($notification->state), StateBall::SIZE_MEDIUM) ]), ' ', FormattedString::create( t('%s on %s', ' on '), HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) ) ]); } $eventInfo = [ new HtmlElement('h2', null, Text::create(t('Event Info'))), new HorizontalKeyValue( t('Sent On'), isset($notification->send_time) ? DateFormatter::formatDateTime($notification->send_time->getTimestamp()) : new EmptyState(t('n. a.')) ) ]; if ($notification->author) { $eventInfo[] = (new HorizontalKeyValue(t('Sent by'), [ new Icon('user'), $notification->author ])); } $eventInfo[] = new HorizontalKeyValue(t('Type'), ucfirst(Str::camel($notification->type))); $eventInfo[] = new HorizontalKeyValue(t('State'), $notification->object_type === 'host' ? ucfirst(HostStates::text($notification->state)) : ucfirst(ServiceStates::text($notification->state))); $eventInfo[] = new HorizontalKeyValue($objectKey, $objectInfo); $notifiedUsers = [new HtmlElement('h2', null, Text::create(t('Notified Contacts')))]; if ($notification->users_notified === 0) { $notifiedUsers[] = new EmptyState(t('None', 'notified users: none')); } elseif (! $this->isPermittedRoute('contacts')) { $notifiedUsers[] = Text::create(sprintf(tp( 'This notification was sent to a single contact', 'This notification was sent to %d contacts', $notification->users_notified ), $notification->users_notified)); } elseif ($notification->users_notified > 0) { $users = $notification->user ->limit(5) ->peekAhead(); $users = $users->execute(); /** @var ResultSet $users */ $notifiedUsers[] = new ObjectList($users); $notifiedUsers[] = (new ShowMore( $users, Links::users()->addParams(['notification_history.id' => bin2hex($notification->id)]), sprintf(t('Show all %d recipients'), $notification->users_notified) ))->setBaseTarget('_next'); } $this->add(ObjectDetailExtensionHook::injectExtensions([ 0 => $pluginOutput, 200 => $eventInfo, 500 => $notifiedUsers ], $this->createExtensions())); } protected function assembleStateChangeEvent(StateHistory $stateChange) { $pluginOutput = []; $commandName = $stateChange->object_type === 'host' ? $this->event->host->checkcommand_name : $this->event->service->checkcommand_name; if (isset($commandName)) { if (empty($stateChange->output) && empty($stateChange->long_output)) { $commandOutput = new EmptyState(t('Output unavailable.')); } else { $commandOutput = new PluginOutputContainer( (new PluginOutput($stateChange->output . "\n" . $stateChange->long_output)) ->setCommandName($commandName) ->setCharacterLimit( (int) Config::module('icingadb') ->get('settings', 'plugin_output_character_limit', 10000) ) ); CopyToClipboard::attachTo($commandOutput); } $pluginOutput = [ new HtmlElement('h2', null, Text::create(t('Plugin Output'))), HtmlElement::create('div', [ 'id' => 'check-output-' . $commandName, 'class' => 'collapsible', 'data-visible-height' => 100 ], $commandOutput) ]; } else { $pluginOutput[] = new EmptyState(t('Waiting for Icinga DB to synchronize the config.')); } if ($stateChange->object_type === 'host') { $objectKey = t('Host'); $objectState = $stateChange->state_type === 'hard' ? $stateChange->hard_state : $stateChange->soft_state; $objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [ HtmlElement::create('span', ['class' => 'state-change'], [ new StateBall(HostStates::text($stateChange->previous_soft_state), StateBall::SIZE_MEDIUM), new StateBall(HostStates::text($objectState), StateBall::SIZE_MEDIUM) ]), ' ', HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) ]); } else { $objectKey = t('Service'); $objectState = $stateChange->state_type === 'hard' ? $stateChange->hard_state : $stateChange->soft_state; $objectInfo = HtmlElement::create('span', ['class' => 'accompanying-text'], [ HtmlElement::create('span', ['class' => 'state-change'], [ new StateBall(ServiceStates::text($stateChange->previous_soft_state), StateBall::SIZE_MEDIUM), new StateBall(ServiceStates::text($objectState), StateBall::SIZE_MEDIUM) ]), ' ', FormattedString::create( t('%s on %s', ' on '), HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) ) ]); } $eventInfo = [ new HtmlElement('h2', null, Text::create(t('Event Info'))), new HorizontalKeyValue( t('Occurred On'), isset($stateChange->event_time) ? DateFormatter::formatDateTime($stateChange->event_time->getTimestamp()) : new EmptyState(t('n. a.')) ), new HorizontalKeyValue(t('Scheduling Source'), $stateChange->scheduling_source), new HorizontalKeyValue(t('Check Source'), $stateChange->check_source) ]; if ($stateChange->state_type === 'soft') { $eventInfo[] = new HorizontalKeyValue(t('Check Attempt'), sprintf( t('%d of %d'), $stateChange->check_attempt, $stateChange->max_check_attempts )); } $eventInfo[] = new HorizontalKeyValue( t('State'), $stateChange->object_type === 'host' ? ucfirst(HostStates::text($objectState)) : ucfirst(ServiceStates::text($objectState)) ); $eventInfo[] = new HorizontalKeyValue( t('State Type'), $stateChange->state_type === 'hard' ? t('Hard', 'state') : t('Soft', 'state') ); $eventInfo[] = new HorizontalKeyValue($objectKey, $objectInfo); $this->add(ObjectDetailExtensionHook::injectExtensions([ 0 => $pluginOutput, 200 => $eventInfo ], $this->createExtensions())); } protected function assembleDowntimeEvent(DowntimeHistory $downtime) { $commentInfo = [ new HtmlElement('h2', null, Text::create(t('Comment'))), new MarkdownText($this->createTicketLinks($downtime->comment)) ]; $eventInfo = [new HtmlElement('h2', null, Text::create(t('Event Info')))]; if ($downtime->triggered_by_id !== null || $downtime->parent_id !== null) { if ($downtime->triggered_by_id !== null) { $label = t('Triggered By'); $relatedDowntime = $downtime->triggered_by; } else { $label = t('Parent'); $relatedDowntime = $downtime->parent; } $query = History::on($this->getDb()) ->columns('id') ->filter(Filter::equal('event_type', 'downtime_start')) ->filter(Filter::equal('history.downtime_history_id', $relatedDowntime->downtime_id)); $this->applyRestrictions($query); if (($relatedEvent = $query->first()) !== null) { /** @var History $relatedEvent */ $eventInfo[] = new HorizontalKeyValue( $label, HtmlElement::create('span', ['class' => 'accompanying-text'], TemplateString::create( $relatedDowntime->is_flexible ? t('{{#link}}Flexible Downtime{{/link}} for %s') : t('{{#link}}Fixed Downtime{{/link}} for %s'), ['link' => new Link(null, Links::event($relatedEvent), ['class' => 'subject'])], ($relatedDowntime->object_type === 'host' ? $this->createHostLink($relatedDowntime->host, true) : $this->createServiceLink($relatedDowntime->service, $relatedDowntime->host, true)) ->addAttributes(['class' => 'subject']) )) ); } } $eventInfo[] = $downtime->object_type === 'host' ? new HorizontalKeyValue(t('Host'), HtmlElement::create( 'span', ['class' => 'accompanying-text'], HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) )) : new HorizontalKeyValue(t('Service'), HtmlElement::create( 'span', ['class' => 'accompanying-text'], FormattedString::create( t('%s on %s', ' on '), HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) ) )); $eventInfo[] = new HorizontalKeyValue( t('Entered On'), isset($downtime->entry_time) ? DateFormatter::formatDateTime($downtime->entry_time->getTimestamp()) : new EmptyState(t('n. a.')) ); $eventInfo[] = new HorizontalKeyValue(t('Author'), [new Icon('user'), $downtime->author]); // TODO: The following should be presented in a specific widget (maybe just like the downtime card) $eventInfo[] = new HorizontalKeyValue( t('Triggered On'), isset($downtime->trigger_time) ? DateFormatter::formatDateTime($downtime->trigger_time->getTimestamp()) : new EmptyState(t('n. a.')) ); $eventInfo[] = new HorizontalKeyValue( t('Scheduled Start'), isset($downtime->scheduled_start_time) ? DateFormatter::formatDateTime($downtime->scheduled_start_time->getTimestamp()) : new EmptyState(t('n. a.')) ); $eventInfo[] = new HorizontalKeyValue( t('Actual Start'), isset($downtime->start_time) ? DateFormatter::formatDateTime($downtime->start_time->getTimestamp()) : new EmptyState(t('n. a.')) ); $eventInfo[] = new HorizontalKeyValue( t('Scheduled End'), isset($downtime->scheduled_end_time) ? DateFormatter::formatDateTime($downtime->scheduled_end_time->getTimestamp()) : new EmptyState(t('n. a.')) ); $eventInfo[] = new HorizontalKeyValue( t('Actual End'), isset($downtime->end_time) ? DateFormatter::formatDateTime($downtime->end_time->getTimestamp()) : new EmptyState(t('n. a.')) ); if ($downtime->is_flexible) { $eventInfo[] = new HorizontalKeyValue(t('Flexible'), t('Yes')); $eventInfo[] = new HorizontalKeyValue( t('Duration'), DateFormatter::formatDuration($downtime->flexible_duration / 1000) ); } $cancelInfo = []; if ($downtime->has_been_cancelled && $downtime->cancel_time) { $cancelInfo = [ new HtmlElement('h2', null, Text::create(t('This downtime has been cancelled'))), new HorizontalKeyValue( t('Cancelled On'), DateFormatter::formatDateTime($downtime->cancel_time->getTimestamp()) ), new HorizontalKeyValue(t('Cancelled by'), [new Icon('user'), $downtime->cancelled_by]) ]; } $this->add(ObjectDetailExtensionHook::injectExtensions([ 200 => $commentInfo, 201 => $eventInfo, 600 => $cancelInfo ], $this->createExtensions())); } protected function assembleCommentEvent(CommentHistory $comment) { $commentInfo = [ new HtmlElement('h2', null, Text::create(t('Comment'))), new MarkdownText($this->createTicketLinks($comment->comment)) ]; $eventInfo = [new HtmlElement('h2', null, Text::create(t('Event Info')))]; $eventInfo[] = $comment->object_type === 'host' ? new HorizontalKeyValue(t('Host'), HtmlElement::create( 'span', ['class' => 'accompanying-text'], HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) )) : new HorizontalKeyValue(t('Service'), HtmlElement::create( 'span', ['class' => 'accompanying-text'], FormattedString::create( t('%s on %s', ' on '), HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) ) )); $eventInfo[] = new HorizontalKeyValue( t('Entered On'), isset($comment->entry_time) ? DateFormatter::formatDateTime($comment->entry_time->getTimestamp()) : new EmptyState(t('n. a.')) ); $eventInfo[] = new HorizontalKeyValue(t('Author'), [new Icon('user'), $comment->author]); $eventInfo[] = new HorizontalKeyValue( t('Expires On'), $comment->expire_time ? DateFormatter::formatDateTime($comment->expire_time->getTimestamp()) : new EmptyState(t('Never')) ); $tiedToAckInfo = []; if ($comment->entry_type === 'ack') { $tiedToAckInfo = [ new HtmlElement('h2', null, Text::create(t('This comment is tied to an acknowledgement'))), new HorizontalKeyValue(t('Sticky'), $comment->is_sticky ? t('Yes') : t('No')), new HorizontalKeyValue(t('Persistent'), $comment->is_persistent ? t('Yes') : t('No')) ]; } $removedInfo = []; if ($comment->has_been_removed && $comment->remove_time) { $removedInfo[] = new HtmlElement('h2', null, Text::create(t('This comment has been removed'))); if ($comment->removed_by) { $removedInfo[] = new HorizontalKeyValue( t('Removed On'), DateFormatter::formatDateTime($comment->remove_time->getTimestamp()) ); $removedInfo[] = new HorizontalKeyValue( t('Removed by'), [new Icon('user'), $comment->removed_by] ); } else { $removedInfo[] = new HorizontalKeyValue( t('Expired On'), DateFormatter::formatDateTime($comment->remove_time->getTimestamp()) ); } } $this->add(ObjectDetailExtensionHook::injectExtensions([ 200 => $commentInfo, 201 => $eventInfo, 500 => $tiedToAckInfo, 600 => $removedInfo ], $this->createExtensions())); } protected function assembleFlappingEvent(FlappingHistory $flapping) { $eventInfo = [ new HtmlElement('h2', null, Text::create(t('Event Info'))), $flapping->object_type === 'host' ? new HorizontalKeyValue(t('Host'), HtmlElement::create( 'span', ['class' => 'accompanying-text'], HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) )) : new HorizontalKeyValue(t('Service'), HtmlElement::create( 'span', ['class' => 'accompanying-text'], FormattedString::create( t('%s on %s', ' on '), HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) ) )), new HorizontalKeyValue( t('Started on'), isset($flapping->start_time) ? DateFormatter::formatDateTime($flapping->start_time->getTimestamp()) : new EmptyState(t('n. a.')) ) ]; if ($this->event->event_type === 'flapping_start') { $eventInfo[] = new HorizontalKeyValue(t('Reason'), sprintf( t('State change rate of %.2f%% exceeded the threshold (%.2f%%)'), $flapping->percent_state_change_start, $flapping->flapping_threshold_high )); } elseif ($flapping->end_time !== null) { $eventInfo[] = new HorizontalKeyValue( t('Ended on'), DateFormatter::formatDateTime($flapping->end_time->getTimestamp()) ); $eventInfo[] = new HorizontalKeyValue(t('Reason'), sprintf( t('State change rate of %.2f%% undercut the threshold (%.2f%%)'), $flapping->percent_state_change_end, $flapping->flapping_threshold_low )); } $this->add(ObjectDetailExtensionHook::injectExtensions([ 200 => $eventInfo ], $this->createExtensions())); } protected function assembleAcknowledgeEvent(AcknowledgementHistory $acknowledgement) { $commentInfo = []; if ($acknowledgement->comment) { $commentInfo = [ new HtmlElement('h2', null, Text::create(t('Comment'))), new MarkdownText($this->createTicketLinks($acknowledgement->comment)) ]; } elseif (! isset($acknowledgement->author)) { $commentInfo[] = new EmptyState(t('This acknowledgement was set before Icinga DB history recording')); } $eventInfo = [ new HtmlElement('h2', null, Text::create(t('Event Info'))), new HorizontalKeyValue( t('Set on'), isset($acknowledgement->set_time) ? DateFormatter::formatDateTime($acknowledgement->set_time->getTimestamp()) : new EmptyState(t('n. a.')) ), new HorizontalKeyValue(t('Author'), $acknowledgement->author ? [new Icon('user'), $acknowledgement->author] : new EmptyState(t('n. a.'))), $acknowledgement->object_type === 'host' ? new HorizontalKeyValue(t('Host'), HtmlElement::create( 'span', ['class' => 'accompanying-text'], HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) )) : new HorizontalKeyValue(t('Service'), HtmlElement::create( 'span', ['class' => 'accompanying-text'], FormattedString::create( t('%s on %s', ' on '), HtmlElement::create('span', ['class' => 'subject'], $this->event->service->display_name), HtmlElement::create('span', ['class' => 'subject'], $this->event->host->display_name) ) )) ]; if ($this->event->event_type === 'ack_set') { $eventInfo[] = new HorizontalKeyValue( t('Expires On'), $acknowledgement->expire_time ? DateFormatter::formatDateTime($acknowledgement->expire_time->getTimestamp()) : new EmptyState(t('Never')) ); $eventInfo[] = new HorizontalKeyValue(t('Sticky'), isset($acknowledgement->is_sticky) ? ($acknowledgement->is_sticky ? t('Yes') : t('No')) : new EmptyState(t('n. a.'))); $eventInfo[] = new HorizontalKeyValue(t('Persistent'), isset($acknowledgement->is_persistent) ? ($acknowledgement->is_persistent ? t('Yes') : t('No')) : new EmptyState(t('n. a.'))); } else { $eventInfo[] = new HorizontalKeyValue( t('Cleared on'), DateFormatter::formatDateTime( $acknowledgement->clear_time !== null ? $acknowledgement->clear_time->getTimestamp() : $this->event->event_time->getTimestamp() ) ); if ($acknowledgement->cleared_by) { $eventInfo[] = new HorizontalKeyValue( t('Cleared by'), [new Icon('user'), $acknowledgement->cleared_by] ); } else { $expired = false; if ($acknowledgement->expire_time) { $now = (new DateTime())->setTimezone(new DateTimeZone('UTC')); $expiresOn = clone $now; $expiresOn->setTimestamp($acknowledgement->expire_time->getTimestamp()); if ($now <= $expiresOn) { $expired = true; $eventInfo[] = new HorizontalKeyValue(t('Removal Reason'), t( 'The acknowledgement expired on %s', DateFormatter::formatDateTime($acknowledgement->expire_time->getTimestamp()) )); } } if (! $expired) { if ($acknowledgement->is_sticky) { $eventInfo[] = new HorizontalKeyValue( t('Reason'), $acknowledgement->object_type === 'host' ? t('Host recovered') : t('Service recovered') ); } else { $eventInfo[] = new HorizontalKeyValue( t('Reason'), $acknowledgement->object_type === 'host' ? t('Host recovered') // Hosts have no other state between UP and DOWN : t('Service changed its state') ); } } } } $this->add(ObjectDetailExtensionHook::injectExtensions([ 200 => $commentInfo, 201 => $eventInfo ], $this->createExtensions())); } protected function createExtensions(): array { return ObjectDetailExtensionHook::loadExtensions($this->event); } protected function assemble() { switch ($this->event->event_type) { case 'notification': $this->assembleNotificationEvent($this->event->notification); break; case 'state_change': $this->assembleStateChangeEvent($this->event->state); break; case 'downtime_start': case 'downtime_end': $this->assembleDowntimeEvent($this->event->downtime); break; case 'comment_add': case 'comment_remove': $this->assembleCommentEvent($this->event->comment); break; case 'flapping_start': case 'flapping_end': $this->assembleFlappingEvent($this->event->flapping); break; case 'ack_set': case 'ack_clear': $this->assembleAcknowledgeEvent($this->event->acknowledgement); break; } } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/HostDetail.php000066400000000000000000000040171516124654200244010ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Common\HostLinks; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\ServicestateSummary; use ipl\Html\Html; use ipl\Stdlib\Filter; use ipl\Web\Widget\EmptyState; class HostDetail extends ObjectDetail { protected $serviceSummary; public function __construct(Host $object, ServicestateSummary $serviceSummary) { parent::__construct($object); $this->serviceSummary = $serviceSummary; } protected function createServiceStatistics(): array { if ($this->serviceSummary->services_total > 0) { $services = (new ServiceStatistics($this->serviceSummary)) ->setUrl(HostLinks::services($this->object)); } else { $services = new EmptyState(t('This host has no services')); } return [Html::tag('h2', t('Services')), $services]; } protected function assemble() { if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { $this->add($this->createPrintHeader()); } $this->add(ObjectDetailExtensionHook::injectExtensions([ 0 => $this->createRootProblems(), 1 => $this->createPluginOutput(), 190 => $this->createServiceStatistics(), 300 => $this->createActions(), 301 => $this->createNotes(), 400 => $this->createComments(), 401 => $this->createDowntimes(), 500 => $this->createGroups(), 501 => $this->createNotifications(), 510 => $this->createAffectedObjects(), 600 => $this->createCheckStatistics(), 601 => $this->createPerformanceData(), 700 => $this->createCustomVars(), 701 => $this->createFeatureToggles() ], $this->createExtensions())); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/HostInspectionDetail.php000066400000000000000000000010761516124654200264370ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Common\ObjectInspectionDetail; class HostInspectionDetail extends ObjectInspectionDetail { protected function assemble() { $this->add([ $this->createSourceLocation(), $this->createLastCheckResult(), $this->createAttributes(), $this->createCustomVariables(), $this->createRedisInfo() ]); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/HostMetaInfo.php000066400000000000000000000045601516124654200247040ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Date\DateFormatter; use Icinga\Module\Icingadb\Model\Host; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\HorizontalKeyValue; use ipl\Web\Widget\VerticalKeyValue; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Web\Widget\Icon; class HostMetaInfo extends BaseHtmlElement { protected $tag = 'div'; protected $defaultAttributes = ['class' => 'object-meta-info']; /** @var Host */ protected $host; public function __construct(Host $host) { $this->host = $host; } protected function assemble() { $this->addHtml( new VerticalKeyValue('host.name', $this->host->name), new HtmlElement( 'div', null, new HorizontalKeyValue( 'host.address', $this->host->address ?: new EmptyState(t('None', 'address')) ), new HorizontalKeyValue( 'host.address6', $this->host->address6 ?: new EmptyState(t('None', 'address')) ) ), new VerticalKeyValue( 'last_state_change', $this->host->state->last_state_change !== null ? DateFormatter::formatDateTime($this->host->state->last_state_change->getTimestamp()) : (new EmptyState(t('n. a.')))->setTag('span') ) ); $collapsible = new HtmlElement('div', Attributes::create([ 'class' => 'collapsible', 'id' => 'object-meta-info', 'data-toggle-element' => '.object-meta-info-control', 'data-visible-height' => 0 ])); $renderHelper = new HtmlDocument(); $renderHelper->addHtml( $this, new HtmlElement( 'button', Attributes::create(['class' => 'object-meta-info-control']), new Icon('angle-double-up', ['class' => 'collapse-icon']), new Icon('angle-double-down', ['class' => 'expand-icon']) ) ); $this->addWrapper($collapsible); $this->addWrapper($renderHelper); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/HostStatistics.php000066400000000000000000000034621516124654200253340ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Chart\Donut; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Widget\HostStateBadges; use ipl\Html\ValidHtml; use ipl\Web\Widget\VerticalKeyValue; use ipl\Html\HtmlString; use ipl\Web\Filter\QueryString; use ipl\Web\Widget\Link; class HostStatistics extends ObjectStatistics { protected $summary; public function __construct($summary) { $this->summary = $summary; } protected function createDonut(): ValidHtml { $donut = (new Donut()) ->addSlice($this->summary->hosts_up, ['class' => 'slice-state-ok']) ->addSlice($this->summary->hosts_down_handled, ['class' => 'slice-state-critical-handled']) ->addSlice($this->summary->hosts_down_unhandled, ['class' => 'slice-state-critical']) ->addSlice($this->summary->hosts_pending, ['class' => 'slice-state-pending']); return HtmlString::create($donut->render()); } protected function createTotal(): ValidHtml { $url = Links::hosts(); if ($this->hasBaseFilter()) { $url->setFilter($this->getBaseFilter()); } return new Link( (new VerticalKeyValue( tp('Host', 'Hosts', $this->summary->hosts_total), $this->shortenAmount($this->summary->hosts_total) ))->setAttribute('title', $this->summary->hosts_total), $url ); } protected function createBadges(): ValidHtml { $badges = new HostStateBadges($this->summary); if ($this->hasBaseFilter()) { $badges->setBaseFilter($this->getBaseFilter()); } return $badges; } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/MultiselectQuickActions.php000066400000000000000000000176171516124654200271630ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Forms\Command\Object\CheckNowForm; use Icinga\Module\Icingadb\Forms\Command\Object\RemoveAcknowledgementForm; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Stdlib\BaseFilter; use ipl\Web\Url; use ipl\Web\Widget\Icon; class MultiselectQuickActions extends BaseHtmlElement { use BaseFilter; use Auth; protected $summary; protected $type; protected $tag = 'ul'; protected $defaultAttributes = ['class' => 'quick-actions']; /** @var bool Whether to allow process check results */ protected $allowToProcessCheckResults = true; /** @var ?string The summary column prefix */ protected $columnPrefix; /** @var ?string The url path for {@see getLink()} method (default: `icingadb/$this->type . 's'`) */ protected $urlPath; public function __construct($type, $summary) { $this->summary = $summary; $this->type = $type; } /** * Set the summary column prefix * * @param string $columnPrefix * * @return $this */ public function setColumnPrefix(string $columnPrefix): self { $this->columnPrefix = $columnPrefix; return $this; } /** * Get the summary column prefix (default: `$this->type . 's'`) * * @return string */ public function getColumnPrefix(): string { if ($this->columnPrefix === null) { $this->columnPrefix = $this->type . 's'; } return $this->columnPrefix; } /** * Set the url path for {@see getLink()} method * * Omits the trailing slashes * * @param string $urlPath * * @return $this */ public function setUrlPath(string $urlPath): self { $this->urlPath = rtrim($urlPath, '/'); return $this; } /** * Get the url path for {@see getLink()} method * * If not set `icingadb/$this->type . 's'` is used * * @return string */ public function getUrlPath(): string { if ($this->urlPath === null) { $this->urlPath = "icingadb/{$this->type}s"; } return $this->urlPath; } /** * Set whether to allow process check results * * @param bool $state * * @return $this */ public function setAllowToProcessCheckResults(bool $state = true): self { $this->allowToProcessCheckResults = $state; return $this; } protected function assemble() { $unacknowledged = "{$this->getColumnPrefix()}_problems_unacknowledged"; $acks = "{$this->getColumnPrefix()}_acknowledged"; $activeChecks = "{$this->getColumnPrefix()}_active_checks_enabled"; $passiveChecks = "{$this->getColumnPrefix()}_passive_checks_enabled"; if ( $this->summary->$unacknowledged > $this->summary->$acks && $this->isGrantedOnType( 'icingadb/command/acknowledge-problem', $this->type, $this->getBaseFilter(), false ) ) { $this->assembleAction( 'acknowledge', t('Acknowledge'), 'check-circle', t('Acknowledge this problem, suppress all future notifications for it and tag it as being handled') ); } if ( $this->summary->$acks > 0 && $this->isGrantedOnType( 'icingadb/command/remove-acknowledgement', $this->type, $this->getBaseFilter(), false ) ) { $removeAckForm = (new RemoveAcknowledgementForm()) ->setAction($this->getLink('removeAcknowledgement')) // TODO: This is a hack as for the button label the count of objects is used. setCount? setMultiple? ->setObjects(array_fill(0, $this->summary->$acks, null)); $this->add(Html::tag('li', $removeAckForm)); } if ( $this->isGrantedOnType('icingadb/command/schedule-check', $this->type, $this->getBaseFilter(), false) || ( ! empty($this->summary->$activeChecks) && $this->isGrantedOnType( 'icingadb/command/schedule-check/active-only', $this->type, $this->getBaseFilter(), false ) ) ) { $this->add(Html::tag('li', (new CheckNowForm())->setAction($this->getLink('checkNow')))); } if ($this->isGrantedOnType('icingadb/command/comment/add', $this->type, $this->getBaseFilter(), false)) { $this->assembleAction( 'addComment', t('Comment'), 'comment', t('Add a new comment') ); } if ( $this->isGrantedOnType( 'icingadb/command/send-custom-notification', $this->type, $this->getBaseFilter(), false ) ) { $this->assembleAction( 'sendCustomNotification', t('Notification'), 'bell', t('Send a custom notification') ); } if ( $this->isGrantedOnType( 'icingadb/command/downtime/schedule', $this->type, $this->getBaseFilter(), false ) ) { $this->assembleAction( 'scheduleDowntime', t('Downtime'), 'plug', t('Schedule a downtime to suppress all problem notifications within a specific period of time') ); } if ( $this->isGrantedOnType('icingadb/command/schedule-check', $this->type, $this->getBaseFilter(), false) || ( ! empty($this->summary->$activeChecks) && $this->isGrantedOnType( 'icingadb/command/schedule-check/active-only', $this->type, $this->getBaseFilter(), false ) ) ) { $this->assembleAction( 'scheduleCheck', t('Reschedule'), 'calendar', t('Schedule the next active check at a different time than the current one') ); } if ( $this->allowToProcessCheckResults && $this->summary->$passiveChecks > 0 && $this->isGrantedOnType( 'icingadb/command/process-check-result', $this->type, $this->getBaseFilter(), false ) ) { $this->assembleAction( 'processCheckresult', t('Process check result'), 'edit', t('Submit passive check result') ); } } protected function assembleAction(string $action, string $label, string $icon, string $title) { $link = Html::tag( 'a', [ 'href' => $this->getLink($action), 'class' => 'action-link', 'title' => $title, 'data-icinga-modal' => true, 'data-no-icinga-ajax' => true ], [ new Icon($icon), $label ] ); $this->add(Html::tag('li', $link)); } protected function getLink(string $action): string { return Url::fromPath($this->getUrlPath() . '/' . $action) ->setFilter($this->getBaseFilter()) ->getAbsoluteUrl(); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/ObjectDetail.php000066400000000000000000000650131516124654200246750ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Application\ClassLoader; use Icinga\Application\Config; use Icinga\Application\Hook; use Icinga\Application\Hook\GrapherHook; use Icinga\Application\Icinga; use Icinga\Application\Logger; use Icinga\Application\Web; use Icinga\Date\DateFormatter; use Icinga\Exception\IcingaException; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Common\HostLinks; use Icinga\Module\Icingadb\Common\Icons; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\Macros; use Icinga\Module\Icingadb\Compat\CompatHost; use Icinga\Module\Icingadb\Model\CustomvarFlat; use Icinga\Module\Icingadb\Model\DependencyEdge; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\UnreachableParent; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Web\Navigation\Action; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Widget\ItemList\TicketLinkObjectList; use Icinga\Module\Icingadb\Widget\MarkdownText; use Icinga\Module\Icingadb\Common\ServiceLinks; use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; use Icinga\Module\Icingadb\Hook\ActionsHook\ObjectActionsHook; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Model\Usergroup; use Icinga\Module\Icingadb\Util\PluginOutput; use Icinga\Module\Icingadb\Widget\ItemList\DowntimeList; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Sql\Expression; use ipl\Sql\Filter\Exists; use ipl\Web\Widget\CopyToClipboard; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\EmptyStateBar; use ipl\Web\Widget\HorizontalKeyValue; use Icinga\Module\Icingadb\Widget\PluginOutputContainer; use Icinga\Module\Icingadb\Widget\TagList; use Icinga\Module\Monitoring\Hook\DetailviewExtensionHook; use Icinga\Web\Navigation\Navigation; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Html\HtmlElement; use ipl\Html\HtmlString; use ipl\Html\Text; use ipl\Orm\ResultSet; use ipl\Stdlib\Filter; use ipl\Web\Widget\Icon; use ipl\Web\Widget\StateBall; use Throwable; class ObjectDetail extends BaseHtmlElement { use Auth; use Database; use Macros; protected $object; protected $compatObject; protected $objectType; protected $defaultAttributes = [ // Class host-detail is kept as the grafana module's iframe.js depends on it 'class' => ['object-detail', 'host-detail'], 'data-pdfexport-page-breaks-at' => 'h2' ]; protected $tag = 'div'; public function __construct($object) { $this->object = $object; $this->objectType = $object instanceof Host ? 'host' : 'service'; } protected function compatObject() { if ($this->compatObject === null) { $this->compatObject = CompatHost::fromModel($this->object); } return $this->compatObject; } protected function createPrintHeader() { $info = [new HorizontalKeyValue(t('Name'), $this->object->name)]; if ($this->objectType === 'host') { $info[] = new HorizontalKeyValue( t('IPv4 Address'), $this->object->address ?: new EmptyState(t('None', 'address')) ); $info[] = new HorizontalKeyValue( t('IPv6 Address'), $this->object->address6 ?: new EmptyState(t('None', 'address')) ); } $info[] = new HorizontalKeyValue(t('State'), [ $this->object->state->getStateTextTranslated(), ' ', new StateBall($this->object->state->getStateText()) ]); $info[] = new HorizontalKeyValue( t('Last State Change'), isset($this->object->state->last_state_change) ? DateFormatter::formatDateTime($this->object->state->last_state_change->getTimestamp()) : new EmptyState(t('n. a.')) ); return [ new HtmlElement('h2', null, Text::create( $this->objectType === 'host' ? t('Host') : t('Service') )), $info ]; } protected function createActions() { $this->fetchCustomVars(); $navigation = new Navigation(); $navigation->load('icingadb-' . $this->objectType . '-action'); /** @var Action $item */ foreach ($navigation as $item) { $item->setObject($this->object); } $monitoringInstalled = Icinga::app()->getModuleManager()->hasInstalled('monitoring'); $obj = $monitoringInstalled ? $this->compatObject() : $this->object; foreach ($this->object->action_url->first()->action_url ?? [] as $url) { $url = $this->expandMacros($url, $obj); $navigation->addItem( Html::wantHtml([ // Add warning to links that open in new tabs, as recommended by WCAG20 G201 new Icon('external-link-alt', ['title' => t('Link opens in a new window')]), $url ])->render(), [ 'target' => '_blank', 'url' => $url, 'renderer' => [ 'NavigationItemRenderer', 'escape_label' => false ] ] ); } $moduleActions = ObjectActionsHook::loadActions($this->object); $nativeExtensionProviders = []; foreach ($moduleActions->getContent() as $item) { if ($item->getAttributes()->has('data-icinga-module')) { $nativeExtensionProviders[$item->getAttributes()->get('data-icinga-module')->getValue()] = true; } } if (Icinga::app()->getModuleManager()->hasInstalled('monitoring')) { foreach (Hook::all('Monitoring\\' . ucfirst($this->objectType) . 'Actions') as $hook) { $moduleName = ClassLoader::extractModuleName(get_class($hook)); if (! isset($nativeExtensionProviders[$moduleName])) { try { $navigation->merge($hook->getNavigation($this->compatObject())); } catch (Throwable $e) { Logger::error("Failed to load legacy action hook: %s\n%s", $e, $e->getTraceAsString()); $navigation->addItem($moduleName, ['label' => IcingaException::describe($e), 'url' => '#']); } } } } if ($moduleActions->isEmpty() && ($navigation->isEmpty() || ! $navigation->hasRenderableItems())) { return null; } return [ Html::tag('h2', t('Actions')), new HtmlString($navigation->getRenderer()->setCssClass('object-detail-actions')->render()), $moduleActions->isEmpty() ? null : $moduleActions ]; } protected function createCheckStatistics(): array { return [ Html::tag('h2', t('Check Statistics')), new CheckStatistics($this->object) ]; } protected function createComments(): array { if ($this->objectType === 'host') { $link = HostLinks::comments($this->object); $relations = ['host', 'host.state']; } else { $link = ServiceLinks::comments($this->object, $this->object->host); $relations = ['service', 'service.state', 'service.host', 'service.host.state']; } $comments = $this->object->comment ->with($relations) ->limit(3) ->peekAhead(); // TODO: This should be automatically done by the model/resolver and added as ON condition $comments->filter(Filter::equal('object_type', $this->objectType)); $comments = $comments->execute(); /** @var ResultSet $comments */ $content = [Html::tag('h2', t('Comments'))]; if ($comments->hasResult()) { $content[] = new TicketLinkObjectList($comments); $content[] = (new ShowMore($comments, $link))->setBaseTarget('_next'); } else { $content[] = new EmptyState(t('No comments created.')); } return $content; } protected function createCustomVars(): array { $content = [Html::tag('h2', t('Custom Variables'))]; $this->fetchCustomVars(); $vars = (new CustomvarFlat())->unFlattenVars($this->object->customvar_flat); if (! empty($vars)) { $content[] = new HtmlElement('div', Attributes::create([ 'id' => $this->objectType . '-customvars', 'class' => 'collapsible', 'data-visible-height' => 200 ]), new CustomVarTable($vars, $this->object)); } else { $content[] = new EmptyState(t('No custom variables configured.')); } return $content; } protected function createDowntimes(): array { if ($this->objectType === 'host') { $link = HostLinks::downtimes($this->object); $relations = ['host', 'host.state']; } else { $link = ServiceLinks::downtimes($this->object, $this->object->host); $relations = ['service', 'service.state', 'service.host', 'service.host.state']; } $downtimes = $this->object->downtime ->with($relations) ->limit(3) ->peekAhead(); // TODO: This should be automatically done by the model/resolver and added as ON condition $downtimes->filter(Filter::equal('object_type', $this->objectType)); $downtimes = $downtimes->execute(); /** @var ResultSet $downtimes */ $content = [Html::tag('h2', t('Downtimes'))]; if ($downtimes->hasResult()) { $content[] = new TicketLinkObjectList($downtimes); $content[] = (new ShowMore($downtimes, $link))->setBaseTarget('_next'); } else { $content[] = new EmptyState(t('No downtimes scheduled.')); } return $content; } protected function createGroups(): array { $groups = [Html::tag('h2', t('Groups'))]; if ($this->objectType === 'host') { $hostgroups = []; if ($this->isPermittedRoute('hostgroups')) { $hostgroups = $this->object->hostgroup; $this->applyRestrictions($hostgroups); } $hostgroupList = new TagList(); foreach ($hostgroups as $hostgroup) { $hostgroupList->addLink($hostgroup->display_name, Links::hostgroup($hostgroup)); } $groups[] = $hostgroupList->hasContent() ? $hostgroupList : new EmptyState(t('Not a member of any host group.')); } else { $servicegroups = []; if ($this->isPermittedRoute('servicegroups')) { $servicegroups = $this->object->servicegroup; $this->applyRestrictions($servicegroups); } $servicegroupList = new TagList(); foreach ($servicegroups as $servicegroup) { $servicegroupList->addLink($servicegroup->display_name, Links::servicegroup($servicegroup)); } $groups[] = $servicegroupList->hasContent() ? $servicegroupList : new EmptyState(t('Not a member of any service group.')); } return $groups; } protected function createNotes() { $navigation = new Navigation(); $notes = trim($this->object->notes); $monitoringInstalled = Icinga::app()->getModuleManager()->hasInstalled('monitoring'); $obj = $monitoringInstalled ? $this->compatObject() : $this->object; foreach ($this->object->notes_url->first()->notes_url ?? [] as $url) { $url = $this->expandMacros($url, $obj); $navigation->addItem( Html::wantHtml([ // Add warning to links that open in new tabs, as recommended by WCAG20 G201 new Icon('external-link-alt', ['title' => t('Link opens in a new window')]), $url ])->render(), [ 'target' => '_blank', 'url' => $url, 'renderer' => [ 'NavigationItemRenderer', 'escape_label' => false ] ] ); } $content = []; if (! $navigation->isEmpty() && $navigation->hasRenderableItems()) { $content[] = new HtmlString($navigation->getRenderer()->setCssClass('object-detail-actions')->render()); } if ($notes !== '') { $content[] = (new MarkdownText($notes)) ->addAttributes([ 'class' => 'collapsible', 'data-visible-height' => 200, 'id' => $this->objectType . '-notes' ]); } if (empty($content)) { return null; } array_unshift($content, Html::tag('h2', t('Notes'))); return $content; } protected function createNotifications(): array { [$users, $usergroups] = $this->getUsersAndUsergroups(); $userList = new TagList(); $usergroupList = new TagList(); foreach ($users as $user) { $userList->addLink([new Icon(Icons::USER), $user->display_name], Links::user($user)); } foreach ($usergroups as $usergroup) { $usergroupList->addLink( [new Icon(Icons::USERGROUP), $usergroup->display_name], Links::usergroup($usergroup) ); } return [ Html::tag('h2', t('Notifications')), new HorizontalKeyValue( t('Contacts'), $userList->hasContent() ? $userList : new EmptyState(t('No contacts configured.')) ), new HorizontalKeyValue( t('Contact Groups'), $usergroupList->hasContent() ? $usergroupList : new EmptyState(t('No contact groups configured.')) ) ]; } protected function createPerformanceData(): array { $content[] = Html::tag('h2', t('Performance Data')); if (empty($this->object->state->performance_data)) { $content[] = new EmptyState(t('No performance data available.')); } else { $content[] = new HtmlElement( 'div', Attributes::create(['id' => 'check-perfdata-' . $this->object->checkcommand_name]), new PerfDataTable($this->object->state->normalized_performance_data) ); } return $content; } protected function createPluginOutput(): array { if (empty($this->object->state->output) && empty($this->object->state->long_output)) { $pluginOutput = new EmptyState(t('Output unavailable.')); } else { $pluginOutput = new PluginOutputContainer( PluginOutput::fromObject($this->object) ->setCharacterLimit( (int) Config::module('icingadb') ->get('settings', 'plugin_output_character_limit', 10000) ) ); CopyToClipboard::attachTo($pluginOutput); } return [ Html::tag('h2', t('Plugin Output')), Html::tag( 'div', [ 'id' => 'check-output-' . $this->object->checkcommand_name, 'class' => 'collapsible', 'data-visible-height' => 100 ], $pluginOutput ) ]; } protected function createExtensions(): array { $extensions = ObjectDetailExtensionHook::loadExtensions($this->object); $nativeExtensionProviders = []; foreach ($extensions as $extension) { if ($extension instanceof BaseHtmlElement && $extension->getAttributes()->has('data-icinga-module')) { $nativeExtensionProviders[$extension->getAttributes()->get('data-icinga-module')->getValue()] = true; } } if (! Icinga::app()->getModuleManager()->hasInstalled('monitoring')) { return $extensions; } foreach (Hook::all('Grapher') as $grapher) { /** @var GrapherHook $grapher */ $moduleName = ClassLoader::extractModuleName(get_class($grapher)); if (isset($nativeExtensionProviders[$moduleName])) { continue; } try { $graph = HtmlString::create($grapher->getPreviewHtml($this->compatObject())); } catch (Throwable $e) { Logger::error("Failed to load legacy grapher: %s\n%s", $e, $e->getTraceAsString()); $graph = Text::create(IcingaException::describe($e)); } $location = ObjectDetailExtensionHook::BASE_LOCATIONS[ObjectDetailExtensionHook::GRAPH_SECTION]; while (isset($extensions[$location])) { $location++; } $extensions[$location] = $graph; } foreach (Hook::all('Monitoring\DetailviewExtension') as $extension) { /** @var DetailviewExtensionHook $extension */ $moduleName = $extension->getModule()->getName(); if (isset($nativeExtensionProviders[$moduleName])) { continue; } try { /** @var Web $app */ $app = Icinga::app(); $renderedExtension = $extension ->setView($app->getViewRenderer()->view) ->getHtmlForObject($this->compatObject()); if (! empty($renderedExtension)) { $extensionHtml = new HtmlElement( 'div', Attributes::create([ 'class' => 'icinga-module module-' . $moduleName, 'data-icinga-module' => $moduleName ]), HtmlString::create($renderedExtension) ); } else { $extensionHtml = HtmlString::create(''); } } catch (Throwable $e) { Logger::error("Failed to load legacy detail extension: %s\n%s", $e, $e->getTraceAsString()); $extensionHtml = Text::create(IcingaException::describe($e)); } $location = ObjectDetailExtensionHook::BASE_LOCATIONS[ObjectDetailExtensionHook::DETAIL_SECTION]; while (isset($extensions[$location])) { $location++; } $extensions[$location] = $extensionHtml; } return $extensions; } protected function createFeatureToggles(): array { $form = new ToggleObjectFeaturesForm($this->object); if ($this->objectType === 'host') { $form->setAction(HostLinks::toggleFeatures($this->object)->getAbsoluteUrl()); } else { $form->setAction(ServiceLinks::toggleFeatures($this->object, $this->object->host)->getAbsoluteUrl()); } return [ Html::tag('h2', t('Feature Commands')), $form ]; } protected function getUsersAndUsergroups(): array { $users = []; $usergroups = []; $groupBy = false; if ($this->objectType === 'host') { $objectFilter = Filter::all( Filter::equal('notification.host_id', $this->object->id), Filter::unlike('notification.service_id', '*') ); $objectFilter->metaData()->set('forceOptimization', false); $groupBy = true; } else { $objectFilter = Filter::equal( 'notification.service_id', $this->object->id ); } $userQuery = null; if ($this->isPermittedRoute('contacts')) { $userQuery = User::on($this->getDb()); $userQuery->filter($objectFilter); $this->applyRestrictions($userQuery); if ($groupBy) { $userQuery->getSelectBase()->groupBy(['user.id']); } foreach ($userQuery as $user) { $users[$user->name] = $user; } } if ($this->isPermittedRoute('contactgroups')) { $usergroupQuery = Usergroup::on($this->getDb()); $usergroupQuery->filter($objectFilter); $this->applyRestrictions($usergroupQuery); if ($groupBy && $userQuery !== null) { $userQuery->getSelectBase()->groupBy(['usergroup.id']); } foreach ($usergroupQuery as $usergroup) { $usergroups[$usergroup->name] = $usergroup; } } return [$users, $usergroups]; } protected function fetchCustomVars() { $customvarFlat = $this->object->customvar_flat; if (! $customvarFlat instanceof ResultSet) { $this->applyRestrictions($customvarFlat); $customvarFlat->withColumns(['customvar.name', 'customvar.value']); $this->object->customvar_flat = $customvarFlat->execute(); } } /** * Create a list of root problems of the object that is unreachable because of dependency failure * * @return ?BaseHtmlElement[] */ protected function createRootProblems(): ?array { if (! Backend::supportsDependencies()) { if ($this->object->state->is_reachable) { return null; } return [ HtmlElement::create('h2', null, Text::create(t('Root Problems'))), new EmptyStateBar(t("You're missing out! Upgrade Icinga DB and see the actual root cause here!")) ]; } // If a dependency has failed, then the children are not reachable. Hence, the root problems should not be shown // if the object is reachable. And in case of a service, since, it may be also be unreachable because of its // host being down, only show its root problems if it's really caused by a dependency failure. if ( $this->object->state->is_reachable || ($this->object instanceof Service && ! $this->object->has_problematic_parent) ) { return null; } $rootProblems = UnreachableParent::on($this->getDb(), $this->object) ->with([ 'redundancy_group', 'redundancy_group.state', 'host', 'host.state', 'host.icon_image', 'host.state.last_comment', 'service', 'service.state', 'service.icon_image', 'service.state.last_comment', 'service.host', 'service.host.state' ]) ->orderBy([ 'host.state.severity', 'host.state.last_state_change', 'service.state.severity', 'service.state.last_state_change', 'redundancy_group.state.failed', 'redundancy_group.state.last_state_change' ], SORT_DESC); $this->applyRestrictions($rootProblems); return [ HtmlElement::create('h2', null, Text::create(t('Root Problems'))), (new ObjectList($rootProblems))->setEmptyStateMessage( t('You are not authorized to view these objects.') ) ]; } /** * Create a list of objects affected by the object that is a part of failed dependency * * @return ?BaseHtmlElement[] */ protected function createAffectedObjects(): ?array { if (! isset($this->object->state->affects_children) || ! $this->object->state->affects_children) { return null; } $affectedObjects = DependencyNode::on($this->getDb()) ->with([ 'redundancy_group', 'redundancy_group.state', 'host', 'host.state', 'host.icon_image', 'host.state.last_comment', 'service', 'service.state', 'service.icon_image', 'service.state.last_comment', 'service.host', 'service.host.state' ]) ->setResultSetClass(VolatileStateResults::class) ->limit(5) ->orderBy([ 'host.state.severity', 'host.state.last_state_change', 'service.state.severity', 'service.state.last_state_change', 'redundancy_group.state.failed', 'redundancy_group.state.last_state_change' ], SORT_DESC); $failedEdges = DependencyEdge::on($this->getDb()) ->utilize('child') ->columns([new Expression('1')]) ->filter(Filter::equal('state.failed', 'y')); if ($this->object instanceof Host) { $failedEdges ->filter(Filter::equal('parent.host.id', $this->object->id)) ->filter(Filter::unlike('parent.service.id', '*')); } else { $failedEdges->filter(Filter::equal('parent.service.id', $this->object->id)); } $failedEdges->getFilter()->metaData()->set('forceOptimization', false); $edgeResolver = $failedEdges->getResolver(); $childAlias = $edgeResolver->getAlias( $edgeResolver->resolveRelation($failedEdges->getModel()->getTableName() . '.child')->getTarget() ); $affectedObjects->filter(new Exists( $failedEdges->assembleSelect() ->where( "$childAlias.id = " . $affectedObjects->getResolver() ->qualifyColumn('id', $affectedObjects->getModel()->getTableName()) ) )); $this->applyRestrictions($affectedObjects); return [ HtmlElement::create('h2', null, Text::create(t('Affected Objects'))), (new ObjectList($affectedObjects))->setEmptyStateMessage( t('You are not authorized to view these objects.') ) ]; } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/ObjectHeader.php000066400000000000000000000073371516124654200246700ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Exception\NotImplementedError; use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Model\Downtime; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Hostgroupsummary; use Icinga\Module\Icingadb\Model\RedundancyGroup; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\ServicegroupSummary; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Model\Usergroup; use Icinga\Module\Icingadb\View\CommentRenderer; use Icinga\Module\Icingadb\View\DowntimeRenderer; use Icinga\Module\Icingadb\View\EventRenderer; use Icinga\Module\Icingadb\View\HostgroupRenderer; use Icinga\Module\Icingadb\View\HostRenderer; use Icinga\Module\Icingadb\View\RedundancyGroupRenderer; use Icinga\Module\Icingadb\View\ServicegroupRenderer; use Icinga\Module\Icingadb\View\ServiceRenderer; use Icinga\Module\Icingadb\View\UsergroupRenderer; use Icinga\Module\Icingadb\View\UserRenderer; use ipl\Html\BaseHtmlElement; use ipl\Orm\Model; use ipl\Web\Layout\HeaderItemLayout; use ipl\Web\Layout\ItemLayout; /** * ObjectHeader * * Create a header for icingadb object * * @phpstan-type _PART1 = RedundancyGroup|Service|Host|Usergroup|User|Comment|Downtime|History * @phpstan-type _PART2 = Hostgroupsummary|ServicegroupSummary * * @template Item of _PART1|_PART2 */ class ObjectHeader extends BaseHtmlElement { protected $defaultAttributes = ['data-base-target' => '_next']; /** @var Item */ protected $object; protected $tag = 'div'; /** * Create a new object header * * @param Item $object */ public function __construct(Model $object) { $this->object = $object; } /** * @throws NotImplementedError When the object type is not supported */ protected function assemble(): void { switch (true) { case $this->object instanceof RedundancyGroup: $renderer = new RedundancyGroupRenderer(); break; case $this->object instanceof Service: $renderer = new ServiceRenderer(); break; case $this->object instanceof Host: $renderer = new HostRenderer(); break; case $this->object instanceof Usergroup: $renderer = new UsergroupRenderer(); break; case $this->object instanceof User: $renderer = new UserRenderer(); break; case $this->object instanceof Comment: $renderer = (new CommentRenderer())->setTicketLinkDisabled(); break; case $this->object instanceof Downtime: $renderer = (new DowntimeRenderer())->setTicketLinkDisabled(); break; case $this->object instanceof History: $renderer = new EventRenderer(); break; case $this->object instanceof Hostgroupsummary: $renderer = new HostgroupRenderer(); break; case $this->object instanceof ServicegroupSummary: $renderer = new ServicegroupRenderer(); break; default: throw new NotImplementedError('Not implemented'); } $layout = new HeaderItemLayout($this->object, $renderer); if (isset($this->object->icon_image->icon_image)) { $layout->after(ItemLayout::VISUAL, 'icon-image'); } $this->addAttributes($layout->getAttributes()); $this->addHtml($layout); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/ObjectStatistics.php000066400000000000000000000030151516124654200256170ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Html\ValidHtml; use ipl\Stdlib\BaseFilter; abstract class ObjectStatistics extends BaseHtmlElement { use BaseFilter; protected $tag = 'ul'; protected $defaultAttributes = ['class' => 'object-statistics']; abstract protected function createDonut(): ValidHtml; abstract protected function createTotal(): ValidHtml; abstract protected function createBadges(): ValidHtml; /** * Shorten the given amount to 4 characters max * * @param int $amount * * @return string */ protected function shortenAmount(int $amount): string { if ($amount < 10000) { return (string) $amount; } if ($amount < 999500) { return sprintf('%dk', round($amount / 1000.0)); } if ($amount < 9959000) { return sprintf('%.1fM', $amount / 1000000.0); } // I think we can rule out amounts over 1 Billion return sprintf('%dM', $amount / 1000000.0); } protected function assemble() { $this->add([ Html::tag('li', ['class' => 'object-statistics-graph'], $this->createDonut()), Html::tag('li', ['class' => ['object-statistics-total', 'text-center']], $this->createTotal()), Html::tag('li', $this->createBadges()) ]); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/ObjectsDetail.php000066400000000000000000000145341516124654200250620ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Chart\Donut; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Forms\Command\Object\ToggleObjectFeaturesForm; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectsDetailExtensionHook; use Icinga\Module\Icingadb\Model\HoststateSummary; use Icinga\Module\Icingadb\Model\ServicestateSummary; use Icinga\Module\Icingadb\Util\FeatureStatus; use Icinga\Module\Icingadb\Widget\HostStateBadges; use Icinga\Module\Icingadb\Widget\ServiceStateBadges; use ipl\Orm\Query; use ipl\Stdlib\BaseFilter; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\VerticalKeyValue; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Html\HtmlString; use ipl\Web\Widget\ActionLink; class ObjectsDetail extends BaseHtmlElement { use BaseFilter; protected $summary; protected $query; protected $type; protected $defaultAttributes = ['class' => 'objects-detail']; protected $tag = 'div'; /** * Construct an object detail summary widget * * @param string $type * @param HoststateSummary|ServicestateSummary $summary * @param Query $query */ public function __construct(string $type, $summary, Query $query) { $this->summary = $summary; $this->query = $query; $this->type = $type; } protected function createChart(): BaseHtmlElement { $content = Html::tag('div', ['class' => 'multiselect-summary']); if ($this->type === 'host') { $hostsChart = (new Donut()) ->addSlice($this->summary->hosts_up, ['class' => 'slice-state-ok']) ->addSlice($this->summary->hosts_down_handled, ['class' => 'slice-state-critical-handled']) ->addSlice($this->summary->hosts_down_unhandled, ['class' => 'slice-state-critical']) ->addSlice($this->summary->hosts_pending, ['class' => 'slice-state-pending']); $badges = (new HostStateBadges($this->summary)) ->setBaseFilter($this->getBaseFilter()); $content->add([ HtmlString::create($hostsChart->render()), new VerticalKeyValue( tp('Host', 'Hosts', $this->summary->hosts_total), $this->summary->hosts_total ), $badges ]); } else { $servicesChart = (new Donut()) ->addSlice($this->summary->services_ok, ['class' => 'slice-state-ok']) ->addSlice($this->summary->services_warning_handled, ['class' => 'slice-state-warning-handled']) ->addSlice($this->summary->services_warning_unhandled, ['class' => 'slice-state-warning']) ->addSlice($this->summary->services_critical_handled, ['class' => 'slice-state-critical-handled']) ->addSlice($this->summary->services_critical_unhandled, ['class' => 'slice-state-critical']) ->addSlice($this->summary->services_unknown_handled, ['class' => 'slice-state-unknown-handled']) ->addSlice($this->summary->services_unknown_unhandled, ['class' => 'slice-state-unknown']) ->addSlice($this->summary->services_pending, ['class' => 'slice-state-pending']); $badges = (new ServiceStateBadges($this->summary)) ->setBaseFilter($this->getBaseFilter()); $content->add([ HtmlString::create($servicesChart->render()), new VerticalKeyValue( tp('Service', 'Services', $this->summary->services_total), $this->summary->services_total ), $badges ]); } return $content; } protected function createComments(): array { $content = [Html::tag('h2', t('Comments'))]; if ($this->summary->comments_total > 0) { $content[] = new ActionLink( sprintf( tp('Show %d comment', 'Show %d comments', $this->summary->comments_total), $this->summary->comments_total ), Links::comments()->setFilter($this->getBaseFilter()) ); } else { $content[] = new EmptyState(t('No comments created.')); } return $content; } protected function createDowntimes(): array { $content = [Html::tag('h2', t('Downtimes'))]; if ($this->summary->downtimes_total > 0) { $content[] = new ActionLink( sprintf( tp('Show %d downtime', 'Show %d downtimes', $this->summary->downtimes_total), $this->summary->downtimes_total ), Links::downtimes()->setFilter($this->getBaseFilter()) ); } else { $content[] = new EmptyState(t('No downtimes scheduled.')); } return $content; } protected function createFeatureToggles(): array { $form = new ToggleObjectFeaturesForm(new FeatureStatus($this->type, $this->summary)); if ($this->type === 'host') { $form->setAction( Links::toggleHostsFeatures() ->setFilter($this->getBaseFilter()) ->getAbsoluteUrl() ); } else { $form->setAction( Links::toggleServicesFeatures() ->setFilter($this->getBaseFilter()) ->getAbsoluteUrl() ); } return [ Html::tag('h2', t('Feature Commands')), $form ]; } protected function createExtensions(): array { return ObjectsDetailExtensionHook::loadExtensions( $this->type, $this->query, $this->getBaseFilter() ); } protected function createSummary(): array { return [ Html::tag('h2', t('Summary')), $this->createChart() ]; } protected function assemble() { $this->add(ObjectsDetailExtensionHook::injectExtensions([ 190 => $this->createSummary(), 400 => $this->createComments(), 401 => $this->createDowntimes(), 701 => $this->createFeatureToggles() ], $this->createExtensions())); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/PerfDataTable.php000066400000000000000000000101361516124654200247760ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Util\PerfData; use Icinga\Module\Icingadb\Util\PerfDataSet; use ipl\Html\Attributes; use ipl\Html\HtmlElement; use ipl\Html\HtmlString; use ipl\Html\Table; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\Icon; class PerfDataTable extends Table { use Translation; protected $defaultAttributes = [ 'class' => 'performance-data-table collapsible', 'data-visible-rows' => 6 ]; /** @var string The perfdata string */ protected $perfdataStr; /** @var int Max labels to show; 0 for no limit */ protected $limit; /** @var string The color indicating the perfdata state */ protected $color; /** * Display the given perfdata string to the user * * @param string $perfdataStr The perfdata string * @param int $limit Max labels to show; 0 for no limit * @param string $color The color indicating the perfdata state */ public function __construct(string $perfdataStr, int $limit = 0, string $color = PerfData::PERFDATA_OK) { $this->perfdataStr = $perfdataStr; $this->limit = $limit; $this->color = $color; } public function assemble() { $pieChartData = PerfDataSet::fromString($this->perfdataStr)->asArray(); $keys = [ '' => '', 'label' => t('Label'), 'value' => t('Value'), 'min' => t('Min'), 'max' => t('Max'), 'warn' => t('Warning'), 'crit' => t('Critical') ]; $containsSparkline = false; foreach ($pieChartData as $perfdata) { if ($perfdata->isVisualizable() || ! $perfdata->isValid()) { $containsSparkline = true; break; } } $headerRow = new HtmlElement('tr'); foreach ($keys as $key => $col) { if (! $containsSparkline && $key === '') { continue; } $headerRow->addHtml(new HtmlElement('th', Attributes::create([ 'class' => $key === 'label' ? 'title' : null ]), Text::create($col))); } $this->getHeader()->addHtml($headerRow); $count = 0; foreach ($pieChartData as $perfdata) { if ($this->limit > 0 && $count === $this->limit) { break; } $count++; $cols = []; if ($containsSparkline) { if ($perfdata->isVisualizable()) { $cols[] = Table::td( HtmlString::create($perfdata->asInlinePie($this->color)->render()), ['class' => 'sparkline-col'] ); } elseif (! $perfdata->isValid()) { $cols[] = Table::td( new Icon( 'triangle-exclamation', [ 'title' => $this->translate( 'Evaluation failed. Performance data is invalid.' ), 'class' => ['invalid-perfdata'] ] ), ['class' => 'sparkline-col'] ); } else { $cols[] = Table::td(''); } } foreach ($perfdata->toArray() as $column => $value) { $cols[] = Table::td( new HtmlElement( 'span', Attributes::create(['class' => $value ? null : 'no-value']), $value ? Text::create($value) : new EmptyState(t('None', 'value')) ), ['class' => $column === 'label' ? 'title' : null] ); } $this->addHtml(Table::tr($cols)); } } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/QuickActions.php000066400000000000000000000117621516124654200247430ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\HostLinks; use Icinga\Module\Icingadb\Common\ServiceLinks; use Icinga\Module\Icingadb\Forms\Command\Object\CheckNowForm; use Icinga\Module\Icingadb\Forms\Command\Object\RemoveAcknowledgementForm; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Web\Widget\Icon; class QuickActions extends BaseHtmlElement { use Auth; /** @var Host|Service */ protected $object; protected $tag = 'ul'; protected $defaultAttributes = ['class' => 'quick-actions']; public function __construct($object) { $this->object = $object; } protected function assemble() { if ($this->object->state->is_problem) { if ($this->object->state->is_acknowledged) { if ($this->isGrantedOn('icingadb/command/remove-acknowledgement', $this->object)) { $removeAckForm = (new RemoveAcknowledgementForm()) ->setAction($this->getLink('removeAcknowledgement')) ->setObjects([$this->object]); $this->add(Html::tag('li', $removeAckForm)); } } elseif ($this->isGrantedOn('icingadb/command/acknowledge-problem', $this->object)) { $this->assembleAction( 'acknowledge', t('Acknowledge'), 'check-circle', t('Acknowledge this problem, suppress all future notifications for it and tag it as being handled') ); } } if ( $this->isGrantedOn('icingadb/command/schedule-check', $this->object) || ( $this->object->active_checks_enabled && $this->isGrantedOn('icingadb/command/schedule-check/active-only', $this->object) ) ) { $this->add(Html::tag('li', (new CheckNowForm())->setAction($this->getLink('checkNow')))); } if ($this->isGrantedOn('icingadb/command/comment/add', $this->object)) { $this->assembleAction( 'addComment', t('Comment', 'verb'), 'comment', t('Add a new comment') ); } if ($this->isGrantedOn('icingadb/command/send-custom-notification', $this->object)) { $this->assembleAction( 'sendCustomNotification', t('Notification'), 'bell', t('Send a custom notification') ); } if ($this->isGrantedOn('icingadb/command/downtime/schedule', $this->object)) { $this->assembleAction( 'scheduleDowntime', t('Downtime'), 'plug', t('Schedule a downtime to suppress all problem notifications within a specific period of time') ); } if ( $this->isGrantedOn('icingadb/command/schedule-check', $this->object) || ( $this->object->active_checks_enabled && $this->isGrantedOn('icingadb/command/schedule-check/active-only', $this->object) ) ) { $this->assembleAction( 'scheduleCheck', t('Reschedule'), 'calendar', t('Schedule the next active check at a different time than the current one') ); } if ( $this->object->passive_checks_enabled && $this->isGrantedOn('icingadb/command/process-check-result', $this->object) ) { $this->assembleAction( 'processCheckresult', t('Process check result'), 'edit', sprintf( t('Submit a one time or so called passive result for the %s check'), $this->object->checkcommand_name ) ); } } protected function assembleAction(string $action, string $label, string $icon, string $title) { $link = Html::tag( 'a', [ 'href' => $this->getLink($action), 'class' => 'action-link', 'title' => $title, 'data-icinga-modal' => true, 'data-no-icinga-ajax' => true ], [ new Icon($icon), $label ] ); $this->add(Html::tag('li', $link)); } protected function getLink($action) { if ($this->object instanceof Host) { return HostLinks::$action($this->object)->getAbsoluteUrl(); } else { return ServiceLinks::$action($this->object, $this->object->host)->getAbsoluteUrl(); } } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/RedundancyGroupDetail.php000066400000000000000000000106141516124654200265750ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\RedundancyGroup; use Icinga\Module\Icingadb\Model\UnreachableParent; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\I18n\Translation; use ipl\Stdlib\Filter; use ipl\Web\Url; class RedundancyGroupDetail extends BaseHtmlElement { use Auth; use Database; use Translation; /** @var RedundancyGroup The redundancy group */ protected $group; protected $defaultAttributes = [ 'class' => ['redundancygroup-detail'], 'data-pdfexport-page-breaks-at' => 'h2' ]; protected $tag = 'div'; /** * Create a new redundancy group detail widget * * @param RedundancyGroup $group */ public function __construct(RedundancyGroup $group) { $this->group = $group; } /** * Create hook extensions * * @return array */ protected function createExtensions(): array { return ObjectDetailExtensionHook::loadExtensions($this->group); } /** * Create a list of root problems if the redundancy group fails * * @return ?BaseHtmlElement[] */ protected function createRootProblems(): ?array { if ($this->group->state->is_reachable) { return null; } $rootProblems = UnreachableParent::on($this->getDb(), $this->group) ->with([ 'redundancy_group', 'redundancy_group.state', 'host', 'host.state', 'host.icon_image', 'host.state.last_comment', 'service', 'service.state', 'service.icon_image', 'service.state.last_comment', 'service.host', 'service.host.state', ]) ->orderBy([ 'host.state.severity', 'host.state.last_state_change', 'service.state.severity', 'service.state.last_state_change', 'redundancy_group.state.failed', 'redundancy_group.state.last_state_change' ], SORT_DESC); $this->applyRestrictions($rootProblems); return [ HtmlElement::create('h2', null, Text::create($this->translate('Root Problems'))), (new ObjectList($rootProblems))->setEmptyStateMessage( $this->translate('You are not authorized to view these objects.') ) ]; } /** * Create a list of group members * * @return BaseHtmlElement[] */ protected function createGroupMembers(): array { $membersQuery = DependencyNode::on($this->getDb()) ->with([ 'host', 'host.state', 'service', 'service.state', 'service.host', 'service.host.state' ]) ->filter(Filter::equal('child.redundancy_group.id', $this->group->id)) ->limit(5) ->peekAhead(); $this->applyRestrictions($membersQuery); // TODO: Do not execute at this time. The widget may be replaced by a hook in which case the result is unused. $members = $membersQuery->execute(); return [ HtmlElement::create('h2', null, Text::create($this->translate('Group Members'))), (new ObjectList($members)) ->setEmptyStateMessage($this->translate('You are not authorized to view these objects.')), (new ShowMore($members, Url::fromPath('icingadb/redundancygroup/members', ['id' => $this->group->id]))) ->setBaseTarget('_self') ]; } protected function assemble(): void { $this->add(ObjectDetailExtensionHook::injectExtensions([ 0 => $this->createRootProblems(), 510 => $this->createGroupMembers(), ], $this->createExtensions())); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/ServiceDetail.php000066400000000000000000000024351516124654200250660ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\Service; class ServiceDetail extends ObjectDetail { public function __construct(Service $object) { parent::__construct($object); } protected function assemble() { if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { $this->add($this->createPrintHeader()); } $this->add(ObjectDetailExtensionHook::injectExtensions([ 0 => $this->createRootProblems(), 1 => $this->createPluginOutput(), 300 => $this->createActions(), 301 => $this->createNotes(), 400 => $this->createComments(), 401 => $this->createDowntimes(), 500 => $this->createGroups(), 501 => $this->createNotifications(), 510 => $this->createAffectedObjects(), 600 => $this->createCheckStatistics(), 601 => $this->createPerformanceData(), 700 => $this->createCustomVars(), 701 => $this->createFeatureToggles() ], $this->createExtensions())); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/ServiceInspectionDetail.php000066400000000000000000000011011516124654200271070ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Common\ObjectInspectionDetail; class ServiceInspectionDetail extends ObjectInspectionDetail { protected function assemble() { $this->add([ $this->createSourceLocation(), $this->createLastCheckResult(), $this->createAttributes(), $this->createCustomVariables(), $this->createRedisInfo() ]); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/ServiceMetaInfo.php000066400000000000000000000040241516124654200253620ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Date\DateFormatter; use Icinga\Module\Icingadb\Model\Service; use ipl\I18n\Translation; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\VerticalKeyValue; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlDocument; use ipl\Html\HtmlElement; use ipl\Web\Widget\Icon; class ServiceMetaInfo extends BaseHtmlElement { use Translation; protected $tag = 'div'; protected $defaultAttributes = ['class' => 'object-meta-info']; /** @var Service */ protected $service; public function __construct(Service $service) { $this->service = $service; } protected function assemble() { $this->addHtml( new VerticalKeyValue('service.name', $this->service->name), new VerticalKeyValue( 'last_state_change', isset($this->service->state->last_state_change) ? DateFormatter::formatDateTime($this->service->state->last_state_change->getTimestamp()) : (new EmptyState($this->translate('n. a.')))->setTag('span') ) ); $collapsible = new HtmlElement('div', Attributes::create([ 'class' => 'collapsible', 'id' => 'object-meta-info', 'data-toggle-element' => '.object-meta-info-control', 'data-visible-height' => 0 ])); $renderHelper = new HtmlDocument(); $renderHelper->addHtml( $this, new HtmlElement( 'button', Attributes::create(['class' => 'object-meta-info-control']), new Icon('angle-double-up', ['class' => 'collapse-icon']), new Icon('angle-double-down', ['class' => 'expand-icon']) ) ); $this->addWrapper($collapsible); $this->addWrapper($renderHelper); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/ServiceStatistics.php000066400000000000000000000055751516124654200260260ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Chart\Donut; use Icinga\Module\Icingadb\Common\HostLinks; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Widget\ServiceStateBadges; use ipl\Html\ValidHtml; use ipl\Web\Url; use ipl\Web\Widget\VerticalKeyValue; use ipl\Html\HtmlString; use ipl\Web\Widget\Link; class ServiceStatistics extends ObjectStatistics { protected $summary; /** @var ?Url */ protected ?Url $url; public function __construct($summary) { $this->summary = $summary; } /** * Return the URL pointing to all matching services. * * If not set, the URL of the services overview is returned as fallback. * * @return Url */ public function getUrl(): Url { return $this->url ?? Links::services(); } /** * Set the URL pointing to all matching services. * * @param Url $url The URL to set. * * @return $this */ public function setUrl(Url $url): self { $this->url = $url; return $this; } protected function createDonut(): ValidHtml { $donut = (new Donut()) ->addSlice($this->summary->services_ok, ['class' => 'slice-state-ok']) ->addSlice($this->summary->services_warning_handled, ['class' => 'slice-state-warning-handled']) ->addSlice($this->summary->services_warning_unhandled, ['class' => 'slice-state-warning']) ->addSlice($this->summary->services_critical_handled, ['class' => 'slice-state-critical-handled']) ->addSlice($this->summary->services_critical_unhandled, ['class' => 'slice-state-critical']) ->addSlice($this->summary->services_unknown_handled, ['class' => 'slice-state-unknown-handled']) ->addSlice($this->summary->services_unknown_unhandled, ['class' => 'slice-state-unknown']) ->addSlice($this->summary->services_pending, ['class' => 'slice-state-pending']); return HtmlString::create($donut->render()); } protected function createTotal(): ValidHtml { $url = $this->getUrl(); if ($this->hasBaseFilter()) { $url->setFilter($this->getBaseFilter()); } return new Link( (new VerticalKeyValue( tp('Service', 'Services', $this->summary->services_total), $this->shortenAmount($this->summary->services_total) ))->setAttribute('title', $this->summary->services_total), $url ); } protected function createBadges(): ValidHtml { $badges = new ServiceStateBadges($this->summary); if ($this->hasBaseFilter()) { $badges->setBaseFilter($this->getBaseFilter()); } return $badges->setUrl($this->getUrl()); } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/UserDetail.php000066400000000000000000000146401516124654200244050ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Html\Attributes; use ipl\Web\Url; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\HorizontalKeyValue; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Html\Text; class UserDetail extends BaseHtmlElement { use Auth; use Database; /** @var User The given user */ protected $user; protected $defaultAttributes = ['class' => 'object-detail']; protected $tag = 'div'; public function __construct(User $user) { $this->user = $user; } protected function createCustomVars(): array { $content = [new HtmlElement('h2', null, Text::create(t('Custom Variables')))]; $flattenedVars = $this->user->customvar_flat; $this->applyRestrictions($flattenedVars); $vars = $this->user->customvar_flat->getModel()->unflattenVars($flattenedVars); if (! empty($vars)) { $content[] = new HtmlElement('div', Attributes::create([ 'id' => 'user-customvars', 'class' => 'collapsible', 'data-visible-height' => 200 ]), new CustomVarTable($vars, $this->user)); } else { $content[] = new EmptyState(t('No custom variables configured.')); } return $content; } protected function createUserDetail(): array { list($hostStates, $serviceStates) = $this->separateStates($this->user->states); $hostStates = implode(', ', $this->localizeStates($hostStates)); $serviceStates = implode(', ', $this->localizeStates($serviceStates)); $types = implode(', ', $this->localizeTypes($this->user->types)); return [ new HtmlElement('h2', null, Text::create(t('Details'))), new HorizontalKeyValue(t('Name'), $this->user->name), new HorizontalKeyValue( t('Timeperiod'), $this->user->timeperiod->display_name ?: new EmptyState(t('None', 'timeperiod')) ), new HorizontalKeyValue(t('Notifications Enabled'), $this->user->notifications_enabled ? t('Yes') : t('No')), new HorizontalKeyValue(t('E-Mail'), $this->user->email ?: new EmptyState(t('None', 'address'))), new HorizontalKeyValue(t('Pager'), $this->user->pager ?: new EmptyState(t('None', 'phone-number'))), new HorizontalKeyValue(t('Host States'), $hostStates ?: t('All')), new HorizontalKeyValue(t('Service States'), $serviceStates ?: t('All')), new HorizontalKeyValue(t('Types'), $types ?: t('All')) ]; } protected function createUsergroupList(): array { $userGroups = $this->user->usergroup->limit(6)->peekAhead()->execute(); $showMoreLink = (new ShowMore( $userGroups, Links::usergroups()->addParams(['user.name' => $this->user->name]) ))->setBaseTarget('_next'); return [ new HtmlElement('h2', null, Text::create(t('Groups'))), new ObjectList($userGroups), $showMoreLink ]; } protected function createExtensions(): array { return ObjectDetailExtensionHook::loadExtensions($this->user); } protected function assemble() { $this->add(ObjectDetailExtensionHook::injectExtensions([ 200 => $this->createUserDetail(), 500 => $this->createUsergroupList(), 700 => $this->createCustomVars() ], $this->createExtensions())); } private function localizeTypes(array $types): array { $localizedTypes = []; foreach ($types as $type) { switch ($type) { case 'problem': $localizedTypes[] = t('Problem'); break; case 'ack': $localizedTypes[] = t('Acknowledgement'); break; case 'recovery': $localizedTypes[] = t('Recovery'); break; case 'downtime_start': $localizedTypes[] = t('Downtime Start'); break; case 'downtime_end': $localizedTypes[] = t('Downtime End'); break; case 'downtime_removed': $localizedTypes[] = t('Downtime Removed'); break; case 'flapping_start': $localizedTypes[] = t('Flapping Start'); break; case 'flapping_end': $localizedTypes[] = t('Flapping End'); break; case 'custom': $localizedTypes[] = t('Custom'); break; } } return $localizedTypes; } private function localizeStates(array $states): array { $localizedState = []; foreach ($states as $state) { switch ($state) { case 'up': $localizedState[] = t('Up'); break; case 'down': $localizedState[] = t('Down'); break; case 'ok': $localizedState[] = t('Ok'); break; case 'warning': $localizedState[] = t('Warning'); break; case 'critical': $localizedState[] = t('Critical'); break; case 'unknown': $localizedState[] = t('Unknown'); break; } } return $localizedState; } private function separateStates(array $states): array { $hostStates = []; $serviceStates = []; foreach ($states as $state) { if ($state === 'Up' || $state === 'Down') { $hostStates[] = $state; } else { $serviceStates[] = $state; } } return [$hostStates, $serviceStates]; } } icingadb-web-1.4.0/library/Icingadb/Widget/Detail/UsergroupDetail.php000066400000000000000000000057311516124654200254630ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\Detail; use Icinga\Module\Icingadb\Common\Auth; use Icinga\Module\Icingadb\Common\Database; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Hook\ExtensionHook\ObjectDetailExtensionHook; use Icinga\Module\Icingadb\Model\Usergroup; use Icinga\Module\Icingadb\Widget\ItemList\ObjectList; use Icinga\Module\Icingadb\Widget\ShowMore; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Web\Url; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\HorizontalKeyValue; class UsergroupDetail extends BaseHtmlElement { use Auth; use Database; /** @var Usergroup The given user group */ protected $usergroup; protected $defaultAttributes = ['class' => 'object-detail']; protected $tag = 'div'; public function __construct(Usergroup $usergroup) { $this->usergroup = $usergroup; } protected function createPrintHeader() { return [ new HtmlElement('h2', null, Text::create(t('Details'))), new HorizontalKeyValue(t('Name'), $this->usergroup->name) ]; } protected function createCustomVars(): array { $content = [new HtmlElement('h2', null, Text::create(t('Custom Variables')))]; $flattenedVars = $this->usergroup->customvar_flat; $this->applyRestrictions($flattenedVars); $vars = $this->usergroup->customvar_flat->getModel()->unflattenVars($flattenedVars); if (! empty($vars)) { $content[] = new HtmlElement('div', Attributes::create([ 'id' => 'usergroup-customvars', 'class' => 'collapsible', 'data-visible-height' => 200 ]), new CustomVarTable($vars, $this->usergroup)); } else { $content[] = new EmptyState(t('No custom variables configured.')); } return $content; } protected function createUserList(): array { $users = $this->usergroup->user->limit(6)->peekAhead()->execute(); $showMoreLink = (new ShowMore( $users, Links::users()->addParams(['usergroup.name' => $this->usergroup->name]) ))->setBaseTarget('_next'); return [ new HtmlElement('h2', null, Text::create(t('Contacts'))), new ObjectList($users), $showMoreLink ]; } protected function createExtensions(): array { return ObjectDetailExtensionHook::loadExtensions($this->usergroup); } protected function assemble() { if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { $this->add($this->createPrintHeader()); } $this->add(ObjectDetailExtensionHook::injectExtensions([ 500 => $this->createUserList(), 700 => $this->createCustomVars() ], $this->createExtensions())); } } icingadb-web-1.4.0/library/Icingadb/Widget/Health.php000066400000000000000000000054631516124654200223520ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Module\Icingadb\ProvidedHook\IcingaHealth; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Web\Widget\TimeAgo; use ipl\Web\Widget\TimeSince; use ipl\Web\Widget\VerticalKeyValue; class Health extends BaseHtmlElement { protected $data; protected $tag = 'section'; public function __construct($data) { $this->data = $data; } protected function assemble() { if ( ! isset($this->data->icingadb_version) || version_compare( IcingaHealth::normalizeVersion($this->data->icingadb_version), IcingaHealth::REQUIRED_ICINGADB_VERSION, '<' ) ) { $this->addHtml(Html::tag('div', ['class' => 'icinga-health down'], [ sprintf( t('Icinga DB is outdated, please upgrade to version %s or later.'), IcingaHealth::REQUIRED_ICINGADB_VERSION ) ])); } elseif ($this->data->heartbeat->getTimestamp() > time() - 60) { $this->add(Html::tag('div', ['class' => 'icinga-health up'], [ Html::sprintf( t('Icinga 2 is up and running %s', '...since '), new TimeSince($this->data->icinga2_start_time->getTimestamp()) ) ])); } else { $this->add(Html::tag('div', ['class' => 'icinga-health down'], [ Html::sprintf( t('Icinga 2 or Icinga DB is not running %s', '...since '), new TimeSince($this->data->heartbeat->getTimestamp()) ) ])); } $icingaInfo = Html::tag('div', ['class' => 'icinga-info'], [ new VerticalKeyValue( t('Icinga 2 Version'), $this->data->icinga2_version ), new VerticalKeyValue( t('Icinga 2 Start Time'), new TimeAgo($this->data->icinga2_start_time->getTimestamp()) ), new VerticalKeyValue( t('Last Heartbeat'), new TimeAgo($this->data->heartbeat->getTimestamp()) ), new VerticalKeyValue( t('Active Icinga 2 Endpoint'), $this->data->endpoint->name ?: t('N/A') ), new VerticalKeyValue( t('Icinga DB Version'), $this->data->icingadb_version ?? t('N/A') ), new VerticalKeyValue( t('Active Icinga Web Endpoint'), gethostname() ?: t('N/A') ) ]); $this->add($icingaInfo); } } icingadb-web-1.4.0/library/Icingadb/Widget/HostStateBadges.php000066400000000000000000000020051516124654200241560ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Module\Icingadb\Common\HostStates; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\StateBadges; use ipl\Web\Url; class HostStateBadges extends StateBadges { protected function getBaseUrl(): Url { return Links::hosts(); } protected function getType(): string { return 'host'; } protected function getPrefix(): string { return 'hosts'; } protected function getStateInt(string $state): int { return HostStates::int($state); } protected function assemble() { $this->addAttributes(['class' => 'host-state-badges']); $this->add(array_filter([ $this->createGroup('down'), $this->createBadge('unknown'), $this->createBadge('up'), $this->createBadge('pending') ])); } } icingadb-web-1.4.0/library/Icingadb/Widget/HostStatusBar.php000066400000000000000000000012061516124654200237020ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Module\Icingadb\Common\BaseStatusBar; use ipl\Html\BaseHtmlElement; class HostStatusBar extends BaseStatusBar { protected function assembleTotal(BaseHtmlElement $total): void { $total->add(sprintf(tp('%d Host', '%d Hosts', $this->summary->hosts_total), $this->summary->hosts_total)); } protected function createStateBadges(): BaseHtmlElement { return (new HostStateBadges($this->summary))->setBaseFilter($this->getBaseFilter()); } } icingadb-web-1.4.0/library/Icingadb/Widget/HostSummaryDonut.php000066400000000000000000000052661516124654200244530ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Chart\Donut; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\HoststateSummary; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Html\HtmlString; use ipl\Html\TemplateString; use ipl\Html\Text; use ipl\Stdlib\BaseFilter; use ipl\Stdlib\Filter; use ipl\Web\Common\Card; use ipl\Web\Filter\QueryString; class HostSummaryDonut extends Card { use BaseFilter; protected $defaultAttributes = ['class' => 'donut-container', 'data-base-target' => '_next']; /** @var HoststateSummary */ protected $summary; public function __construct(HoststateSummary $summary) { $this->summary = $summary; } protected function assembleBody(BaseHtmlElement $body) { $labelBigUrlFilter = Filter::all( Filter::equal('host.state.soft_state', 1), Filter::equal('host.state.is_handled', 'n') ); if ($this->hasBaseFilter()) { $labelBigUrlFilter->add($this->getBaseFilter()); } $donut = (new Donut()) ->addSlice($this->summary->hosts_up, ['class' => 'slice-state-ok']) ->addSlice($this->summary->hosts_down_handled, ['class' => 'slice-state-critical-handled']) ->addSlice($this->summary->hosts_down_unhandled, ['class' => 'slice-state-critical']) ->addSlice($this->summary->hosts_pending, ['class' => 'slice-state-pending']) ->setLabelBig((string) $this->summary->hosts_down_unhandled) ->setLabelBigUrl(Links::hosts()->setFilter($labelBigUrlFilter)->addParams([ 'sort' => 'host.state.last_state_change' ])) ->setLabelBigEyeCatching($this->summary->hosts_down_unhandled > 0) ->setLabelSmall(t('Down')); $body->addHtml( new HtmlElement('div', Attributes::create(['class' => 'donut']), new HtmlString($donut->render())) ); } protected function assembleFooter(BaseHtmlElement $footer) { $footer->addHtml((new HostStateBadges($this->summary))->setBaseFilter($this->getBaseFilter())); } protected function assembleHeader(BaseHtmlElement $header) { $header->addHtml( new HtmlElement('h2', null, Text::create(t('Hosts'))), new HtmlElement('span', Attributes::create(['class' => 'meta']), TemplateString::create( t('{{#total}}Total{{/total}} %d'), ['total' => new HtmlElement('span')], (int) $this->summary->hosts_total )) ); } } icingadb-web-1.4.0/library/Icingadb/Widget/IconImage.php000066400000000000000000000035511516124654200227740ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlDocument; use ipl\Web\Url; use ipl\Web\Widget\Icon; class IconImage extends BaseHtmlElement { /** @var string */ protected $source; /** @var ?string */ protected $alt; protected $tag = 'img'; /** * Create a new icon image * * @param string $source * @param ?string $alt The alternative text */ public function __construct(string $source, ?string $alt) { $this->source = $source; $this->alt = $alt; } public function renderUnwrapped() { if (! $this->getAttributes()->has('src')) { // If it's an icon we don't need the tag return ''; } return parent::renderUnwrapped(); } protected function assemble() { $src = $this->source; if (strpos($src, '.') === false) { $this->setWrapper((new HtmlDocument())->addHtml(new Icon($src))); return; } if (strpos($src, '/') === false) { $src = 'img/icons/' . $src; } if (getenv('ICINGAWEB_EXPORT_FORMAT') === 'pdf') { $srcUrl = Url::fromPath($src); $srcPath = $srcUrl->getRelativeUrl(); if (! $srcUrl->isExternal() && file_exists($srcPath) && is_file($srcPath)) { $mimeType = @mime_content_type($srcPath); $content = @file_get_contents($srcPath); if ($mimeType !== false && $content !== false) { $src = "data:$mimeType;base64," . base64_encode($content); } } } $this->addAttributes([ 'src' => $src, 'alt' => $this->alt ]); } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemList/000077500000000000000000000000001516124654200221565ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Widget/ItemList/CommandTransportList.php000066400000000000000000000013031516124654200270130ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemList; use Icinga\Module\Icingadb\Common\DetailActions; use ipl\Web\Common\BaseOrderedItemList; use ipl\Web\Url; class CommandTransportList extends BaseOrderedItemList { use DetailActions; protected function init(): void { $this->getAttributes()->add('class', 'command-transport-list'); $this->initializeDetailActions(); $this->setDetailUrl(Url::fromPath('icingadb/command-transport/show')); } protected function getItemClass(): string { return CommandTransportListItem::class; } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemList/CommandTransportListItem.php000066400000000000000000000044071516124654200276420ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemList; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Stdlib\Filter; use ipl\Web\Common\BaseOrderedListItem; use ipl\Web\Url; use ipl\Web\Widget\Icon; use ipl\Web\Widget\Link; class CommandTransportListItem extends BaseOrderedListItem { protected function init(): void { $this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)); } protected function assembleHeader(BaseHtmlElement $header): void { } protected function assembleMain(BaseHtmlElement $main): void { $main->addHtml(new Link( new HtmlElement('strong', null, Text::create($this->item->name)), Url::fromPath('icingadb/command-transport/show', ['name' => $this->item->name]) )); $main->addHtml(new Link( new Icon('trash', ['title' => sprintf(t('Remove command transport "%s"'), $this->item->name)]), Url::fromPath('icingadb/command-transport/remove', ['name' => $this->item->name]), [ 'class' => 'pull-right action-link', 'data-icinga-modal' => true, 'data-no-icinga-ajax' => true ] )); if ($this->getOrder() + 1 < $this->list->count()) { $main->addHtml((new Link( new Icon('arrow-down'), Url::fromPath('icingadb/command-transport/sort', [ 'name' => $this->item->name, 'pos' => $this->getOrder() + 1 ]), ['class' => 'pull-right action-link'] ))->setBaseTarget('_self')); } if ($this->getOrder() > 0) { $main->addHtml((new Link( new Icon('arrow-up'), Url::fromPath('icingadb/command-transport/sort', [ 'name' => $this->item->name, 'pos' => $this->getOrder() - 1 ]), ['class' => 'pull-right action-link'] ))->setBaseTarget('_self')); } } protected function createVisual(): ?BaseHtmlElement { return null; } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemList/LoadMoreObjectList.php000066400000000000000000000023141516124654200263540ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemList; use Icinga\Exception\NotImplementedError; use Icinga\Module\Icingadb\Common\LoadMore; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Model\NotificationHistory; use Icinga\Module\Icingadb\View\EventRenderer; use Icinga\Module\Icingadb\View\NotificationRenderer; use ipl\Orm\Model; use ipl\Orm\ResultSet; use ipl\Web\Widget\ItemList; /** * LoadMoreObjectList * * Create a list of icingadb objects with Load more link * * @template Item of NotificationHistory|History * * @extends ObjectList */ class LoadMoreObjectList extends ObjectList { use LoadMore; public function __construct(ResultSet $data) { ItemList::__construct($data, function (Model $item) { if ($item instanceof NotificationHistory) { return new NotificationRenderer(); } elseif ($item instanceof History) { return new EventRenderer(); } throw new NotImplementedError('Not implemented'); }); $this->data = $this->getIterator($data); } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemList/ObjectList.php000066400000000000000000000217371516124654200247430ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemList; use Icinga\Exception\NotImplementedError; use Icinga\Module\Icingadb\Common\DetailActions; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Model\DependencyNode; use Icinga\Module\Icingadb\Model\Downtime; use Icinga\Module\Icingadb\Model\History; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\NotificationHistory; use Icinga\Module\Icingadb\Model\RedundancyGroup; use Icinga\Module\Icingadb\Model\Service; use Icinga\Module\Icingadb\Model\User; use Icinga\Module\Icingadb\Model\Usergroup; use Icinga\Module\Icingadb\Redis\VolatileStateResults; use Icinga\Module\Icingadb\View\CommentRenderer; use Icinga\Module\Icingadb\View\DowntimeRenderer; use Icinga\Module\Icingadb\View\HostRenderer; use Icinga\Module\Icingadb\View\RedundancyGroupRenderer; use Icinga\Module\Icingadb\View\ServiceRenderer; use Icinga\Module\Icingadb\View\UsergroupRenderer; use Icinga\Module\Icingadb\View\UserRenderer; use Icinga\Module\Icingadb\Widget\Notice; use InvalidArgumentException; use ipl\Html\HtmlDocument; use ipl\Orm\Model; use ipl\Stdlib\Filter; use ipl\Web\Layout\DetailedItemLayout; use ipl\Web\Layout\ItemLayout; use ipl\Web\Layout\MinimalItemLayout; use ipl\Web\Url; use ipl\Web\Widget\ItemList; /** * ObjectList * * Create a list of icingadb objects * * @template Result of DependencyNode|Service|Host|Usergroup|User|Comment|Downtime * @template Item of RedundancyGroup|Service|Host|Usergroup|User|Comment|Downtime = Result * * @extends ItemList */ class ObjectList extends ItemList { use DetailActions; /** @var bool Whether the list contains at least one item with an icon_image */ protected $hasIconImages = false; public function __construct($data) { parent::__construct($data, function (Model $item) { if ($item instanceof RedundancyGroup) { return new RedundancyGroupRenderer(); } elseif ($item instanceof Service) { return new ServiceRenderer(); } elseif ($item instanceof Host) { return new HostRenderer(); } elseif ($item instanceof Usergroup) { return new UsergroupRenderer(); } elseif ($item instanceof User) { return new UserRenderer(); } elseif ($item instanceof Comment) { return new CommentRenderer(); } elseif ($item instanceof Downtime) { return new DowntimeRenderer(); } throw new NotImplementedError('Not implemented'); }); } protected function init(): void { $this->initializeDetailActions(); } /** * Get whether the list contains at least one item with an icon_image * * @return bool */ public function hasIconImages(): bool { return $this->hasIconImages; } /** * Set whether the list contains at least one item with an icon_image * * @param bool $hasIconImages * * @return $this */ public function setHasIconImages(bool $hasIconImages): self { $this->hasIconImages = $hasIconImages; return $this; } /** * Set the view mode * * @param 'minimal'|'common'|'detailed' $mode * * @return $this */ public function setViewMode(string $mode): self { switch ($mode) { case 'minimal': $this->setItemLayoutClass(MinimalItemLayout::class); break; case 'detailed': $this->setItemLayoutClass(DetailedItemLayout::class); break; case 'common': $this->setItemLayoutClass(ItemLayout::class); break; default: throw new InvalidArgumentException('Invalid view mode'); } return $this; } public function getItemLayout($item): ItemLayout { $layout = parent::getItemLayout($item); if ($this->hasIconImages()) { $layout->after(ItemLayout::VISUAL, 'icon-image'); } if ($item instanceof Downtime) { $layout->before(ItemLayout::HEADER, 'progress'); } return $layout; } protected function createListItem(object $data) { $isDependencyNodeList = false; if ($data instanceof DependencyNode) { $isDependencyNodeList = true; if (isset($data->redundancy_group_id)) { $object = $data->redundancy_group; } else { $object = isset($data->service_id) ? $data->service : $data->host; } } else { $object = $data; } if (isset($object->icon_image->icon_image)) { $this->setHasIconImages(true); } $item = parent::createListItem($object); if ($this->getDetailActionsDisabled()) { return $item; } switch (true) { case $object instanceof RedundancyGroup: $this->addDetailFilterAttribute($item, Filter::equal('id', bin2hex($object->id))); break; case $object instanceof Service: $this ->setDetailUrl(Url::fromPath('icingadb/service')) ->addDetailFilterAttribute( $item, Filter::all( Filter::equal('name', $object->name), Filter::equal('host.name', $object->host->name) ) ); if (! $isDependencyNodeList) { $this ->setMultiselectUrl(Links::servicesDetails()) ->addMultiSelectFilterAttribute( $item, Filter::all( Filter::equal('service.name', $object->name), Filter::equal('host.name', $object->host->name) ) ); } break; case $object instanceof Host: $this ->setDetailUrl(Url::fromPath('icingadb/host')) ->addDetailFilterAttribute($item, Filter::equal('name', $object->name)); if (! $isDependencyNodeList) { $this ->setMultiselectUrl(Links::hostsDetails()) ->addMultiSelectFilterAttribute($item, Filter::equal('host.name', $object->name)); } break; case $data instanceof User: $this ->setDetailUrl(Url::fromPath('icingadb/contact')) ->addDetailFilterAttribute($item, Filter::equal('name', $object->name)); break; case $object instanceof Usergroup: $this ->setDetailUrl(Url::fromPath('icingadb/contactgroup')) ->addDetailFilterAttribute($item, Filter::equal('name', $object->name)); break; case $object instanceof Comment: $this ->setDetailUrl(Url::fromPath('icingadb/comment')) ->setMultiselectUrl(Links::commentsDetails()) ->addDetailFilterAttribute($item, Filter::equal('name', $object->name)) ->addMultiSelectFilterAttribute($item, Filter::equal('comment.name', $object->name)); break; case $object instanceof Downtime: $this ->setDetailUrl(Url::fromPath('icingadb/downtime')) ->setMultiselectUrl(Links::downtimesDetails()) ->addDetailFilterAttribute($item, Filter::equal('name', $object->name)) ->addMultiSelectFilterAttribute($item, Filter::equal('downtime.name', $object->name)); break; case $object instanceof NotificationHistory: $this ->setDetailUrl(Url::fromPath('icingadb/event')) ->addDetailFilterAttribute($item, Filter::equal('id', bin2hex($object->history->id))); break; case $object instanceof History: $this ->setDetailUrl(Url::fromPath('icingadb/event')) ->addDetailFilterAttribute($item, Filter::equal('id', bin2hex($object->id))); break; } return $item; } protected function assemble(): void { parent::assemble(); if ($this->data instanceof VolatileStateResults && $this->data->isRedisUnavailable()) { $this->prependWrapper((new HtmlDocument())->addHtml(new Notice( $this->translate('Redis is currently unavailable. The shown information might be outdated.') ))); } } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemList/PageSeparatorItem.php000066400000000000000000000014701516124654200262450ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemList; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; class PageSeparatorItem extends BaseHtmlElement { protected $defaultAttributes = ['class' => 'list-item page-separator']; /** @var int */ protected $pageNumber; /** @var string */ protected $tag = 'li'; public function __construct(int $pageNumber) { $this->pageNumber = $pageNumber; } protected function assemble() { $this->add(Html::tag( 'a', [ 'id' => 'page-' . $this->pageNumber, 'data-icinga-no-scroll-on-focus' => true ], $this->pageNumber )); } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemList/TicketLinkObjectList.php000066400000000000000000000021051516124654200267110ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemList; use Icinga\Exception\NotImplementedError; use Icinga\Module\Icingadb\Model\Comment; use Icinga\Module\Icingadb\Model\Downtime; use Icinga\Module\Icingadb\View\CommentRenderer; use Icinga\Module\Icingadb\View\DowntimeRenderer; use ipl\Orm\Model; use ipl\Web\Widget\ItemList; /** * TicketLinkObjectList * * Create a list of icingadb objects with ticket links * * @template Result of Comment|Downtime * * @extends ObjectList */ class TicketLinkObjectList extends ObjectList { public function __construct($data) { ItemList::__construct($data, function (Model $item) { if ($item instanceof Comment) { return (new CommentRenderer())->setNoObjectLink(); } elseif ($item instanceof Downtime) { return (new DowntimeRenderer())->setNoObjectLink(); } throw new NotImplementedError('Not implemented'); }); } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemTable/000077500000000000000000000000001516124654200222725ustar00rootroot00000000000000icingadb-web-1.4.0/library/Icingadb/Widget/ItemTable/BaseStateRowItem.php000066400000000000000000000056541516124654200261770ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemTable; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Orm\Model; /** @todo Figure out what this might (should) have in common with the new BaseTableRowItem implementation */ abstract class BaseStateRowItem extends BaseHtmlElement { protected $defaultAttributes = ['class' => 'row-item']; /** @var Model */ protected $item; /** @var StateItemTable */ protected $list; protected $tag = 'tr'; /** * Create a new row item * * @param Model $item * @param StateItemTable $list */ public function __construct(Model $item, StateItemTable $list) { $this->item = $item; $this->list = $list; $this->init(); } /** * Initialize the row item * * If you want to adjust the row item after construction, override this method. */ protected function init() { } abstract protected function assembleVisual(BaseHtmlElement $visual); abstract protected function assembleCell(BaseHtmlElement $cell, string $path, $value); protected function createVisual(): BaseHtmlElement { $visual = new HtmlElement('td', Attributes::create(['class' => 'visual'])); $this->assembleVisual($visual); return $visual; } protected function assemble() { $this->addHtml($this->createVisual()); foreach ($this->list->getColumns() as $columnPath => $_) { $steps = explode('.', $columnPath); if ($steps[0] === $this->item->getTableName()) { array_shift($steps); $columnPath = implode('.', $steps); } $column = null; $subject = $this->item; foreach ($steps as $i => $step) { if (isset($subject->$step)) { if ($subject->$step instanceof Model) { $subject = $subject->$step; } else { $column = $step; } } else { $columnCandidate = implode('.', array_slice($steps, $i)); if (isset($subject->$columnCandidate)) { $column = $columnCandidate; } else { break; } } } $value = null; if ($column !== null) { $value = $subject->$column; if (is_array($value)) { $value = empty($value) ? null : implode(',', $value); } } $cell = new HtmlElement('td'); if ($value !== null) { $this->assembleCell($cell, $columnPath, $value); } $this->addHtml($cell); } } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemTable/HostItemTable.php000066400000000000000000000013511516124654200255070ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemTable; use Icinga\Module\Icingadb\Common\DetailActions; use Icinga\Module\Icingadb\Common\Links; use ipl\Web\Url; class HostItemTable extends StateItemTable { use DetailActions; protected function init() { $this->initializeDetailActions(); $this->setMultiselectUrl(Links::hostsDetails()); $this->setDetailUrl(Url::fromPath('icingadb/host')); } protected function getItemClass(): string { return HostRowItem::class; } protected function getVisualColumn(): string { return 'host.state.severity'; } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemTable/HostRowItem.php000066400000000000000000000030071516124654200252270ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemTable; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Host; use ipl\Html\BaseHtmlElement; use ipl\Stdlib\Filter; use ipl\Web\Widget\Link; class HostRowItem extends StateRowItem { /** @var HostItemTable */ protected $list; /** @var Host */ protected $item; protected function init() { parent::init(); $this->list->addDetailFilterAttribute($this, Filter::equal('name', $this->item->name)) ->addMultiselectFilterAttribute($this, Filter::equal('host.name', $this->item->name)); } protected function assembleCell(BaseHtmlElement $cell, string $path, $value) { switch ($path) { case 'name': case 'display_name': $cell->addHtml(new Link($this->item->$path, Links::host($this->item), [ 'class' => 'subject', 'title' => $this->item->$path ])); break; case 'service.name': case 'service.display_name': $column = substr($path, 8); $cell->addHtml(new Link( $this->item->service->$column, Links::service($this->item->service, $this->item) )); break; default: parent::assembleCell($cell, $path, $value); } } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemTable/ObjectGrid.php000066400000000000000000000077601516124654200250310ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemTable; use Icinga\Exception\NotImplementedError; use Icinga\Module\Icingadb\Common\DetailActions; use Icinga\Module\Icingadb\Model\Hostgroupsummary; use Icinga\Module\Icingadb\Model\ServicegroupSummary; use InvalidArgumentException; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Html\HtmlElement; use ipl\Html\Text; use ipl\Html\ValidHtml; use ipl\I18n\Translation; use ipl\Orm\ResultSet; use ipl\Stdlib\Filter; use ipl\Web\Common\ItemRenderer; use ipl\Web\Layout\ItemLayout; use ipl\Web\Url; use ipl\Web\Widget\EmptyStateBar; /** * ObjectGrid * * @internal The only reason this class exists is due to the detail actions. In case those are part of the ipl * some time, this class is obsolete, and we must be able to safely drop it. * * @template Item of Hostgroupsummary|ServicegroupSummary */ class ObjectGrid extends BaseHtmlElement { use DetailActions; use Translation; protected $defaultAttributes = [ 'class' => 'object-grid', 'data-base-target' => '_next' ]; protected $tag = 'ul'; /** @var ItemRenderer */ protected $itemRenderer; /** @var ResultSet|iterable */ protected $data; /** @var ?ValidHtml Message to show if the list is empty */ protected $emptyStateMessage; /** * Create a new object grid * * @param ResultSet|iterable $data * @param ItemRenderer $renderer */ public function __construct($data, ItemRenderer $renderer) { if (! is_iterable($data)) { throw new InvalidArgumentException('Data must be an array or an instance of Traversable'); } $this->data = $data; $this->itemRenderer = $renderer; $this->initializeDetailActions(); } /** * Get message to show if the list is empty * * @return ValidHtml */ public function getEmptyStateMessage(): ValidHtml { if ($this->emptyStateMessage === null) { return new Text($this->translate('No items found.')); } return $this->emptyStateMessage; } /** * Set message to show if the list is empty * * @param mixed $message If empty, the default message is used * * @return $this */ public function setEmptyStateMessage($message): self { if (empty($message)) { $this->emptyStateMessage = null; } else { $this->emptyStateMessage = Html::wantHtml($message); } return $this; } /** * Create a list item for the given data * * @param Item $data * * @return ValidHtml * * @throws NotImplementedError When the data is not of the expected type */ protected function createListItem(object $data): ValidHtml { $layout = new ItemLayout($data, $this->itemRenderer); $item = new HtmlElement('li', $layout->getAttributes(), $layout); if ($this->getDetailActionsDisabled()) { return $item; } switch (true) { case $data instanceof Hostgroupsummary: $this->setDetailUrl(Url::fromPath('icingadb/hostgroup')); break; case $data instanceof ServicegroupSummary: $this->setDetailUrl(Url::fromPath('icingadb/servicegroup')); break; default: throw new NotImplementedError('Not implemented'); } $this->addDetailFilterAttribute($item, Filter::equal('name', $data->name)); return $item; } protected function assemble(): void { /** @var Item $data */ foreach ($this->data as $data) { $this->addHtml($this->createListItem($data)); } if ($this->isEmpty()) { $this->setTag('div'); $this->addHtml(new EmptyStateBar($this->getEmptyStateMessage())); } } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemTable/ObjectTable.php000066400000000000000000000034701516124654200251650ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemTable; use Icinga\Exception\NotImplementedError; use Icinga\Module\Icingadb\Common\DetailActions; use Icinga\Module\Icingadb\Model\Hostgroupsummary; use Icinga\Module\Icingadb\Model\ServicegroupSummary; use ipl\Html\ValidHtml; use ipl\Stdlib\Filter; use ipl\Web\Url; use ipl\Web\Widget\ItemTable; /** * ObjectTable * * @internal The only reason this class exists is due to the detail actions. In case those are part of the ipl * some time, this class is obsolete, and we must be able to safely drop it. * * @template Item of Hostgroupsummary|ServicegroupSummary * * @extends ItemTable */ class ObjectTable extends ItemTable { use DetailActions; protected function init(): void { parent::init(); $this->initializeDetailActions(); } /** * @param Item $data * * @return ValidHtml * * @throws NotImplementedError When the data is not of the expected type */ protected function createListItem(object $data): ValidHtml { $item = parent::createListItem($data); if ($this->getDetailActionsDisabled()) { return $item; } switch (true) { case $data instanceof Hostgroupsummary: $this->setDetailUrl(Url::fromPath('icingadb/hostgroup')); break; case $data instanceof ServicegroupSummary: $this->setDetailUrl(Url::fromPath('icingadb/servicegroup')); break; default: throw new NotImplementedError('Not implemented'); } $this->addDetailFilterAttribute($item, Filter::equal('name', $data->name)); return $item; } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemTable/ServiceItemTable.php000066400000000000000000000013701516124654200261730ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemTable; use Icinga\Module\Icingadb\Common\DetailActions; use Icinga\Module\Icingadb\Common\Links; use ipl\Web\Url; class ServiceItemTable extends StateItemTable { use DetailActions; protected function init() { $this->initializeDetailActions(); $this->setMultiselectUrl(Links::servicesDetails()); $this->setDetailUrl(Url::fromPath('icingadb/service')); } protected function getItemClass(): string { return ServiceRowItem::class; } protected function getVisualColumn(): string { return 'service.state.severity'; } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemTable/ServiceRowItem.php000066400000000000000000000035341516124654200257170ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemTable; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\Service; use ipl\Html\BaseHtmlElement; use ipl\Stdlib\Filter; use ipl\Web\Widget\Link; class ServiceRowItem extends StateRowItem { /** @var ServiceItemTable */ protected $list; /** @var Service */ protected $item; protected function init() { parent::init(); $this->list->addMultiselectFilterAttribute( $this, Filter::all( Filter::equal('service.name', $this->item->name), Filter::equal('host.name', $this->item->host->name) ) ); $this->list->addDetailFilterAttribute( $this, Filter::all( Filter::equal('name', $this->item->name), Filter::equal('host.name', $this->item->host->name) ) ); } protected function assembleCell(BaseHtmlElement $cell, string $path, $value) { switch ($path) { case 'name': case 'display_name': $cell->addHtml(new Link( $this->item->$path, Links::service($this->item, $this->item->host), [ 'class' => 'subject', 'title' => $this->item->$path ] )); break; case 'host.name': case 'host.display_name': $column = substr($path, 5); $cell->addHtml(new Link($this->item->host->$column, Links::host($this->item->host))); break; default: parent::assembleCell($cell, $path, $value); } } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemTable/StateItemTable.php000066400000000000000000000156741516124654200256670ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemTable; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\Form; use ipl\Html\Html; use ipl\Html\HtmlElement; use ipl\Html\HtmlString; use ipl\Html\Text; use ipl\Html\ValidHtml; use ipl\I18n\Translation; use ipl\Orm\Common\SortUtil; use ipl\Orm\Query; use ipl\Web\Control\SortControl; use ipl\Web\Widget\EmptyStateBar; use ipl\Web\Widget\Icon; /** @todo Figure out what this might (should) have in common with the new ItemTable implementation */ abstract class StateItemTable extends BaseHtmlElement { use Translation; protected $baseAttributes = [ 'class' => 'state-item-table' ]; /** @var array The columns to render */ protected $columns; /** @var iterable The datasource */ protected $data; /** @var string The sort rules */ protected $sort; protected $tag = 'table'; /** @var ?ValidHtml Message to show if the list is empty */ protected $emptyStateMessage; /** * Create a new item table * * @param iterable $data Datasource of the table * @param array $columns The columns to render, keys are labels */ public function __construct(iterable $data, array $columns) { $this->data = $data; $this->columns = array_flip($columns); $this->addAttributes($this->baseAttributes); $this->init(); } /** * Initialize the item table * * If you want to adjust the item table after construction, override this method. */ protected function init() { } /** * Get message to show if the list is empty * * @return ValidHtml */ public function getEmptyStateMessage(): ValidHtml { if ($this->emptyStateMessage === null) { return new Text($this->translate('No items found.')); } return $this->emptyStateMessage; } /** * Set message to show if the list is empty * * @param mixed $message If empty, the default message is used * * @return $this */ public function setEmptyStateMessage($message): self { if (empty($message)) { $this->emptyStateMessage = null; } else { $this->emptyStateMessage = Html::wantHtml($message); } return $this; } /** * Get the columns being rendered * * @return array */ public function getColumns(): array { return $this->columns; } /** * Set sort rules (as returned by {@see SortControl::getSort()}) * * @param ?string $sort * * @return $this */ public function setSort(?string $sort): self { $this->sort = $sort; return $this; } abstract protected function getItemClass(): string; abstract protected function getVisualColumn(): string; protected function getVisualLabel() { return new Icon('heartbeat', ['title' => $this->translate('Severity')]); } protected function assembleColumnHeader(BaseHtmlElement $header, string $name, $label): void { $sortRules = []; if ($this->sort !== null) { $sortRules = SortUtil::createOrderBy($this->sort); } $active = false; $sortDirection = null; foreach ($sortRules as $rule) { if ($rule[0] === $name) { $sortDirection = $rule[1]; $active = true; break; } } if ($sortDirection === 'desc') { $value = "$name asc"; } else { $value = "$name desc"; } $icon = 'sort'; if ($active) { $icon = $sortDirection === 'desc' ? 'sort-up' : 'sort-down'; } $form = new Form(); $form->setAttribute('method', 'GET'); $button = $form->createElement('button', 'sort', [ 'value' => $value, 'type' => 'submit', 'title' => is_string($label) ? $label : null, 'class' => $active ? 'active' : null ]); $button->addHtml( Html::tag( 'span', null, // With   to have the height sized the same as the others $label ?? HtmlString::create(' ') ), new Icon($icon) ); $form->addElement($button); $header->add($form); switch (true) { case substr($name, -7) === '.output': case substr($name, -12) === '.long_output': $header->getAttributes()->add('class', 'has-plugin-output'); break; case substr($name, -22) === '.icon_image.icon_image': $header->getAttributes()->add('class', 'has-icon-images'); break; case substr($name, -17) === '.performance_data': case substr($name, -28) === '.normalized_performance_data': $header->getAttributes()->add('class', 'has-performance-data'); break; } } protected function assemble() { $itemClass = $this->getItemClass(); $headerRow = new HtmlElement('tr'); $visualCell = new HtmlElement('th', Attributes::create(['class' => 'has-visual'])); $this->assembleColumnHeader($visualCell, $this->getVisualColumn(), $this->getVisualLabel()); $headerRow->addHtml($visualCell); foreach ($this->columns as $name => $label) { $headerCell = new HtmlElement('th'); $this->assembleColumnHeader($headerCell, $name, is_int($label) ? $name : $label); $headerRow->addHtml($headerCell); } $this->addHtml(new HtmlElement('thead', null, $headerRow)); $body = new HtmlElement('tbody', Attributes::create(['data-base-target' => '_next'])); foreach ($this->data as $item) { $body->addHtml(new $itemClass($item, $this)); } if ($body->isEmpty()) { $body->addHtml(new HtmlElement( 'tr', null, new HtmlElement( 'td', Attributes::create(['colspan' => count($this->columns) + 1]), new EmptyStateBar($this->getEmptyStateMessage()) ) )); } $this->addHtml($body); } /** * Enrich the given list of column names with appropriate labels * * @param Query $query * @param array $columns * * @return array */ public static function applyColumnMetaData(Query $query, array $columns): array { $newColumns = []; foreach ($columns as $columnPath) { $label = $query->getResolver()->getColumnDefinition($columnPath)->getLabel(); $newColumns[$label ?? $columnPath] = $columnPath; } return $newColumns; } } icingadb-web-1.4.0/library/Icingadb/Widget/ItemTable/StateRowItem.php000066400000000000000000000137511516124654200254010ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget\ItemTable; use Icinga\Module\Icingadb\Common\HostStates; use Icinga\Module\Icingadb\Common\Icons; use Icinga\Module\Icingadb\Common\ServiceStates; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Util\PerfDataSet; use Icinga\Module\Icingadb\Util\PluginOutput; use Icinga\Module\Icingadb\Widget\CheckAttempt; use Icinga\Module\Icingadb\Widget\IconImage; use Icinga\Module\Icingadb\Widget\PluginOutputContainer; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Html\HtmlString; use ipl\Html\Text; use ipl\Web\Widget\EmptyState; use ipl\Web\Widget\Icon; use ipl\Web\Widget\StateBall; use ipl\Web\Widget\TimeSince; use ipl\Web\Widget\TimeUntil; abstract class StateRowItem extends BaseStateRowItem { /** @var StateItemTable */ protected $list; protected function assembleVisual(BaseHtmlElement $visual) { $stateBall = new StateBall($this->item->state->getStateText(), StateBall::SIZE_LARGE); $icon = $this->item->state->getIcon(); if ($icon === null) { if (! $this->item->notifications_enabled) { $icon = new Icon(Icons::NOTIFICATIONS_DISABLED, [ 'title' => sprintf( '%s (%s)', strtoupper($this->item->state->getStateTextTranslated()), t('has notifications disabled') ) ]); } elseif (! $this->item->active_checks_enabled) { $icon = new Icon(Icons::ACTIVE_CHECKS_DISABLED, [ 'title' => sprintf( '%s (%s)', strtoupper($this->item->state->getStateTextTranslated()), t('has active checks disabled') ) ]); } } $stateBall->add($icon); $stateBall->setHandled($this->item->state->is_problem && ( $this->item->state->is_handled || ! $this->item->state->is_reachable )); $visual->addHtml($stateBall); if ($this->item->state->state_type === 'soft') { $visual->addHtml(new CheckAttempt( (int) $this->item->state->check_attempt, (int) $this->item->max_check_attempts )); } } protected function assembleCell(BaseHtmlElement $cell, string $path, $value) { switch (true) { case $path === 'state.output': case $path === 'state.long_output': if (empty($value)) { $pluginOutput = new EmptyState(t('Output unavailable.')); } else { $pluginOutput = new PluginOutputContainer(PluginOutput::fromObject($this->item)); } $cell->addHtml($pluginOutput) ->getAttributes() ->add('class', 'has-plugin-output'); break; case $path === 'state.soft_state': case $path === 'state.hard_state': case $path === 'state.previous_soft_state': case $path === 'state.previous_hard_state': $stateType = substr($path, 6); if ($this->item instanceof Host) { $stateName = HostStates::translated($this->item->state->$stateType); } else { $stateName = ServiceStates::translated($this->item->state->$stateType); } $cell->addHtml(Text::create($stateName)); break; case $path === 'state.last_update': case $path === 'state.last_state_change': $column = substr($path, 6); $cell->addHtml( isset($this->item->state->$column) ? new TimeSince($this->item->state->$column->getTimestamp()) : new EmptyState(t('n. a.')) ); break; case $path === 'state.next_check': case $path === 'state.next_update': $column = substr($path, 6); $cell->addHtml( isset($this->item->state->$column) ? new TimeUntil($this->item->state->$column->getTimestamp()) : new EmptyState(t('n. a.')) ); break; case $path === 'state.performance_data': case $path === 'state.normalized_performance_data': $perfdataContainer = new HtmlElement('div', Attributes::create(['class' => 'performance-data'])); $pieChartData = PerfDataSet::fromString($this->item->state->normalized_performance_data)->asArray(); foreach ($pieChartData as $perfdata) { if ($perfdata->isVisualizable()) { $perfdataContainer->addHtml(new HtmlString($perfdata->asInlinePie()->render())); } } $cell->addHtml($perfdataContainer) ->getAttributes() ->add('class', 'has-performance-data'); break; case $path === 'is_volatile': case $path === 'host.is_volatile': case substr($path, -8) == '_enabled': case (bool) preg_match('/state\.(is_|in_)/', $path): if ($value) { $cell->addHtml(new Icon('check')); } break; case $path === 'icon_image.icon_image': $cell->addHtml(new IconImage($value, $this->item->icon_image_alt)) ->getAttributes() ->add('class', 'has-icon-images'); break; default: if (preg_match('/(^id|_id|.id|_checksum|_bin)$/', $path)) { $value = bin2hex($value); } $cell->addHtml(Text::create($value)); } } } icingadb-web-1.4.0/library/Icingadb/Widget/MarkdownLine.php000066400000000000000000000012511516124654200235260ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Web\Helper\Markdown; use ipl\Html\BaseHtmlElement; use ipl\Html\DeferredText; class MarkdownLine extends BaseHtmlElement { protected $tag = 'section'; protected $defaultAttributes = ['class' => ['markdown', 'inline']]; /** * MarkdownLine constructor. * * @param string $line */ public function __construct(string $line) { $this->add((new DeferredText(function () use ($line) { return Markdown::line($line); }))->setEscaped(true)); } } icingadb-web-1.4.0/library/Icingadb/Widget/MarkdownText.php000066400000000000000000000012351516124654200235650ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Web\Helper\Markdown; use ipl\Html\BaseHtmlElement; use ipl\Html\DeferredText; class MarkdownText extends BaseHtmlElement { protected $tag = 'section'; protected $defaultAttributes = ['class' => 'markdown']; /** * MarkdownText constructor. * * @param string $text */ public function __construct(string $text) { $this->add((new DeferredText(function () use ($text) { return Markdown::text($text); }))->setEscaped(true)); } } icingadb-web-1.4.0/library/Icingadb/Widget/Notice.php000066400000000000000000000014721516124654200223620ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Web\Widget\Icon; class Notice extends BaseHtmlElement { /** @var mixed */ protected $content; protected $tag = 'p'; protected $defaultAttributes = ['class' => 'notice']; /** * Create a html notice * * @param mixed $content */ public function __construct($content) { $this->content = $content; } protected function assemble(): void { $this->addHtml(new Icon('triangle-exclamation')); $this->addHtml((new HtmlElement('span'))->add($this->content)); $this->addHtml(new Icon('triangle-exclamation')); } } icingadb-web-1.4.0/library/Icingadb/Widget/PluginOutputContainer.php000066400000000000000000000011571516124654200254630ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Module\Icingadb\Util\PluginOutput; use ipl\Html\BaseHtmlElement; class PluginOutputContainer extends BaseHtmlElement { protected $tag = 'div'; public function __construct(PluginOutput $output) { $this->setHtmlContent($output); $this->getAttributes()->registerAttributeCallback('class', function () use ($output) { return $output->isHtml() ? 'plugin-output' : 'plugin-output preformatted'; }); } } icingadb-web-1.4.0/library/Icingadb/Widget/ServiceStateBadges.php000066400000000000000000000021111516124654200246370ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Common\ServiceStates; use Icinga\Module\Icingadb\Common\StateBadges; use ipl\Web\Url; class ServiceStateBadges extends StateBadges { protected function getBaseUrl(): Url { return Links::services(); } protected function getType(): string { return 'service'; } protected function getPrefix(): string { return 'services'; } protected function getStateInt(string $state): int { return ServiceStates::int($state); } protected function assemble() { $this->addAttributes(['class' => 'service-state-badges']); $this->add(array_filter([ $this->createGroup('critical'), $this->createGroup('warning'), $this->createGroup('unknown'), $this->createBadge('ok'), $this->createBadge('pending') ])); } } icingadb-web-1.4.0/library/Icingadb/Widget/ServiceStatusBar.php000066400000000000000000000012721516124654200243700ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Module\Icingadb\Common\BaseStatusBar; use ipl\Html\BaseHtmlElement; class ServiceStatusBar extends BaseStatusBar { protected function assembleTotal(BaseHtmlElement $total): void { $total->add(sprintf( tp('%d Service', '%d Services', $this->summary->services_total), $this->summary->services_total )); } protected function createStateBadges(): BaseHtmlElement { return (new ServiceStateBadges($this->summary))->setBaseFilter($this->getBaseFilter()); } } icingadb-web-1.4.0/library/Icingadb/Widget/ServiceSummaryDonut.php000066400000000000000000000062051516124654200251300ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use Icinga\Chart\Donut; use Icinga\Module\Icingadb\Common\Links; use Icinga\Module\Icingadb\Model\ServicestateSummary; use ipl\Html\Attributes; use ipl\Html\BaseHtmlElement; use ipl\Html\HtmlElement; use ipl\Html\HtmlString; use ipl\Html\TemplateString; use ipl\Html\Text; use ipl\Stdlib\BaseFilter; use ipl\Stdlib\Filter; use ipl\Web\Common\Card; class ServiceSummaryDonut extends Card { use BaseFilter; protected $defaultAttributes = ['class' => 'donut-container', 'data-base-target' => '_next']; /** @var ServicestateSummary */ protected $summary; public function __construct(ServicestateSummary $summary) { $this->summary = $summary; } protected function assembleBody(BaseHtmlElement $body) { $labelBigUrlFilter = Filter::all( Filter::equal('service.state.soft_state', 2), Filter::equal('service.state.is_handled', 'n') ); if ($this->hasBaseFilter()) { $labelBigUrlFilter->add($this->getBaseFilter()); } $donut = (new Donut()) ->addSlice($this->summary->services_ok, ['class' => 'slice-state-ok']) ->addSlice($this->summary->services_warning_handled, ['class' => 'slice-state-warning-handled']) ->addSlice($this->summary->services_warning_unhandled, ['class' => 'slice-state-warning']) ->addSlice($this->summary->services_critical_handled, ['class' => 'slice-state-critical-handled']) ->addSlice($this->summary->services_critical_unhandled, ['class' => 'slice-state-critical']) ->addSlice($this->summary->services_unknown_handled, ['class' => 'slice-state-unknown-handled']) ->addSlice($this->summary->services_unknown_unhandled, ['class' => 'slice-state-unknown']) ->addSlice($this->summary->services_pending, ['class' => 'slice-state-pending']) ->setLabelBig((string) $this->summary->services_critical_unhandled) ->setLabelBigUrl(Links::services()->setFilter($labelBigUrlFilter)->addParams([ 'sort' => 'service.state.last_state_change' ])) ->setLabelBigEyeCatching($this->summary->services_critical_unhandled > 0) ->setLabelSmall(t('Critical')); $body->addHtml( new HtmlElement('div', Attributes::create(['class' => 'donut']), new HtmlString($donut->render())) ); } protected function assembleFooter(BaseHtmlElement $footer) { $footer->addHtml((new ServiceStateBadges($this->summary))->setBaseFilter($this->getBaseFilter())); } protected function assembleHeader(BaseHtmlElement $header) { $header->addHtml( new HtmlElement('h2', null, Text::create(t('Services'))), new HtmlElement('span', Attributes::create(['class' => 'meta']), TemplateString::create( t('{{#total}}Total{{/total}} %d'), ['total' => new HtmlElement('span')], (int) $this->summary->services_total )) ); } } icingadb-web-1.4.0/library/Icingadb/Widget/ShowMore.php000066400000000000000000000025201516124654200226770ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use ipl\Html\BaseHtmlElement; use ipl\Orm\ResultSet; use ipl\Web\Common\BaseTarget; use ipl\Web\Url; use ipl\Web\Widget\ActionLink; class ShowMore extends BaseHtmlElement { use BaseTarget; protected $defaultAttributes = ['class' => 'show-more']; protected $tag = 'div'; /** @var ResultSet */ protected $resultSet; /** @var Url */ protected $url; /** @var ?string */ protected $label; public function __construct(ResultSet $resultSet, Url $url, ?string $label = null) { $this->label = $label; $this->resultSet = $resultSet; $this->url = $url; } public function setLabel(string $label): self { $this->label = $label; return $this; } public function getLabel(): string { return $this->label ?: t('Show More'); } public function renderUnwrapped(): string { if ($this->resultSet->hasMore()) { return parent::renderUnwrapped(); } return ''; } protected function assemble(): void { if ($this->resultSet->hasMore()) { $this->addHtml(new ActionLink($this->getLabel(), $this->url)); } } } icingadb-web-1.4.0/library/Icingadb/Widget/StateBadge.php000066400000000000000000000004311516124654200231360ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; /** @deprecated Use {@see \ipl\Web\Widget\StateBadge} instead */ class StateBadge extends \ipl\Web\Widget\StateBadge { } icingadb-web-1.4.0/library/Icingadb/Widget/StateChange.php000066400000000000000000000063171516124654200233320ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use ipl\Html\BaseHtmlElement; use ipl\Web\Widget\Icon; use ipl\Web\Widget\StateBall; class StateChange extends BaseHtmlElement { protected $previousState; protected $state; protected $previousStateBallSize = StateBall::SIZE_BIG; protected $currentStateBallSize = StateBall::SIZE_BIG; protected $defaultAttributes = ['class' => 'state-change']; protected $tag = 'div'; /** @var ?Icon Current state ball icon */ protected $icon; /** @var bool Whether the state is handled */ protected $isHandled = false; public function __construct(string $state, string $previousState) { $this->previousState = $previousState; $this->state = $state; } /** * Set the state ball size for the previous state * * @param string $size * * @return $this */ public function setPreviousStateBallSize(string $size): self { $this->previousStateBallSize = $size; return $this; } /** * Set the state ball size for the current state * * @param string $size * * @return $this */ public function setCurrentStateBallSize(string $size): self { $this->currentStateBallSize = $size; return $this; } /** * Set the current state ball icon * * @param $icon * * @return $this */ public function setIcon($icon): self { $this->icon = $icon; return $this; } /** * Set whether the current state is handled * * @return $this */ public function setHandled($isHandled = true): self { $this->isHandled = $isHandled; return $this; } protected function assemble() { $currentStateBall = (new StateBall($this->state, $this->currentStateBallSize)) ->setHandled($this->isHandled) ->add($this->icon); $previousStateBall = new StateBall($this->previousState, $this->previousStateBallSize); if ($this->isRightBiggerThanLeft()) { $this->getAttributes()->add('class', 'reversed-state-balls'); $this->addHtml($currentStateBall, $previousStateBall); } else { $this->addHtml($previousStateBall, $currentStateBall); } } protected function isRightBiggerThanLeft(): bool { $left = $this->previousStateBallSize; $right = $this->currentStateBallSize; if ($left === $right) { return false; } elseif ($left === StateBall::SIZE_LARGE) { return false; } $map = [ StateBall::SIZE_BIG => [false, [StateBall::SIZE_LARGE]], StateBall::SIZE_MEDIUM_LARGE => [false, [StateBall::SIZE_BIG, StateBall::SIZE_LARGE]], StateBall::SIZE_MEDIUM => [true, [StateBall::SIZE_TINY, StateBall::SIZE_SMALL]], StateBall::SIZE_SMALL => [true, [StateBall::SIZE_TINY]] ]; list($negate, $sizes) = $map[$left]; $found = in_array($right, $sizes, true); return ($negate && ! $found) || (! $negate && $found); } } icingadb-web-1.4.0/library/Icingadb/Widget/TagList.php000066400000000000000000000013731516124654200225100ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Icinga\Module\Icingadb\Widget; use ipl\Html\BaseHtmlElement; use ipl\Html\Html; use ipl\Web\Widget\Link; class TagList extends BaseHtmlElement { protected $content = []; protected $defaultAttributes = ['class' => 'tag-list', 'data-base-target' => '_next']; protected $tag = 'div'; public function addLink($content, $url): self { $this->content[] = new Link($content, $url); return $this; } public function hasContent(): bool { return ! empty($this->content); } protected function assemble() { $this->add(Html::wrapEach($this->content, 'li')); } } icingadb-web-1.4.0/module.info000066400000000000000000000003421516124654200161760ustar00rootroot00000000000000Module: icingadb Version: 1.4.0 Requires: Libraries: icinga-php-library (>=0.19.0), icinga-php-thirdparty (>=0.15.0) Description: Icinga DB Web UI for Icinga DB – Provides a graphical interface to your Icinga monitoring icingadb-web-1.4.0/phpstan-baseline.neon000066400000000000000000011427451516124654200201710ustar00rootroot00000000000000parameters: ignoreErrors: - message: '#^Cannot cast mixed to int\.$#' identifier: cast.int count: 1 path: application/controllers/CommandTransportController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommandTransportController\:\:addAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommandTransportController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommandTransportController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommandTransportController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommandTransportController\:\:removeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommandTransportController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommandTransportController\:\:showAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommandTransportController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommandTransportController\:\:sortAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommandTransportController.php - message: '#^Parameter \#2 \$filter of static method Icinga\\Data\\Filter\\Filter\:\:where\(\) expects string, mixed given\.$#' identifier: argument.type count: 2 path: application/controllers/CommandTransportController.php - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' identifier: argument.type count: 2 path: application/controllers/CommandTransportController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:\$comment \(Icinga\\Module\\Icingadb\\Model\\Comment\) does not accept ipl\\Orm\\Model\.$#' identifier: assign.propertyType count: 1 path: application/controllers/CommentController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentsController\:\:completeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentsController\:\:deleteAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentsController\:\:detailsAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentsController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentsController\:\:searchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentsController.php - message: '#^Parameter \#1 \$peekAhead of method ipl\\Orm\\Query\:\:peekAhead\(\) expects bool, null given\.$#' identifier: argument.type count: 1 path: application/controllers/CommentsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ConfigController\:\:addFormToContent\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ConfigController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ConfigController\:\:databaseAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ConfigController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ConfigController\:\:redisAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ConfigController.php - message: '#^Parameter \#1 \$name of method ipl\\Web\\Widget\\Tabs\:\:add\(\) expects string, string\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/ConfigController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimeController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:\$downtime \(Icinga\\Module\\Icingadb\\Model\\Downtime\) does not accept ipl\\Orm\\Model\.$#' identifier: assign.propertyType count: 1 path: application/controllers/DowntimeController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimesController\:\:completeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimesController\:\:deleteAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimesController\:\:detailsAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimesController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimesController\:\:searchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimesController.php - message: '#^Parameter \#1 \$peekAhead of method ipl\\Orm\\Query\:\:peekAhead\(\) expects bool, null given\.$#' identifier: argument.type count: 1 path: application/controllers/DowntimesController.php - message: '#^Cannot access property \$exception on mixed\.$#' identifier: property.nonObject count: 1 path: application/controllers/ErrorController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ErrorController\:\:errorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ErrorController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ErrorController\:\:postDispatchXhr\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ErrorController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ErrorController\:\:prepareInit\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ErrorController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\EventController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/EventController.php - message: '#^Parameter \#1 \$string of function hex2bin expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/EventController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\EventController\:\:\$event \(Icinga\\Module\\Icingadb\\Model\\History\) does not accept ipl\\Orm\\Model\.$#' identifier: assign.propertyType count: 1 path: application/controllers/EventController.php - message: '#^Cannot access property \$hosts_active_checks_enabled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: application/controllers/HealthController.php - message: '#^Cannot access property \$hosts_passive_checks_enabled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: application/controllers/HealthController.php - message: '#^Cannot access property \$services_active_checks_enabled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: application/controllers/HealthController.php - message: '#^Cannot access property \$services_passive_checks_enabled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: application/controllers/HealthController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HealthController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HealthController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HistoryController\:\:completeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HistoryController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HistoryController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HistoryController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HistoryController\:\:searchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HistoryController.php - message: '#^Parameter \#2 \$default of method Icinga\\Web\\UrlParams\:\:shift\(\) expects string\|null, int\<1, max\> given\.$#' identifier: argument.type count: 1 path: application/controllers/HistoryController.php - message: '#^Parameter \#2 \$value of method Icinga\\Web\\Url\:\:setParam\(\) expects array\|bool\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/HistoryController.php - message: '#^Parameter \#2 \$value of static method ipl\\Stdlib\\Filter\:\:lessThanOrEqual\(\) expects float\|int\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/HistoryController.php - message: '#^Cannot access property \$is_overdue on mixed\.$#' identifier: property.nonObject count: 4 path: application/controllers/HostController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:acknowledgeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:addCommentAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:assertIsGrantedOnCommandTargets\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:checkNowAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:processCheckresultAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:removeAcknowledgementAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:scheduleCheckAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:scheduleDowntimeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:sendCustomNotificationAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:toggleFeaturesAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostController.php - message: '#^Parameter \#1 \$array of function reset expects array\|object, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/HostController.php - message: '#^Parameter \#1 \$form of method Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:handleCommandForm\(\) expects Icinga\\Module\\Icingadb\\Forms\\Command\\CommandForm\|string, Icinga\\Module\\Icingadb\\Forms\\Command\\Object\\ToggleObjectFeaturesForm\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/HostController.php - message: '#^Parameter \#2 \$apiResult of class Icinga\\Module\\Icingadb\\Widget\\Detail\\HostInspectionDetail constructor expects array, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/HostController.php - message: '#^Parameter \#2 \$default of method Icinga\\Web\\UrlParams\:\:shift\(\) expects string\|null, int\<1, max\> given\.$#' identifier: argument.type count: 1 path: application/controllers/HostController.php - message: '#^Parameter \#2 \$serviceSummary of class Icinga\\Module\\Icingadb\\Widget\\Detail\\HostDetail constructor expects Icinga\\Module\\Icingadb\\Model\\ServicestateSummary, ipl\\Orm\\Model\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/HostController.php - message: '#^Parameter \#2 \$value of method Icinga\\Web\\Url\:\:setParam\(\) expects array\|bool\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/HostController.php - message: '#^Parameter \#2 \$value of static method ipl\\Stdlib\\Filter\:\:lessThanOrEqual\(\) expects float\|int\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/HostController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\HostController\:\:\$commandTargetModel \(ipl\\Orm\\Model\) in isset\(\) is not nullable\.$#' identifier: isset.property count: 1 path: application/controllers/HostController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\HostgroupController\:\:\$hostgroupName \(string\) does not accept mixed\.$#' identifier: assign.propertyType count: 1 path: application/controllers/HostgroupController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostgroupsController\:\:completeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostgroupsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostgroupsController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostgroupsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostgroupsController\:\:searchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostgroupsController.php - message: '#^Parameter \#1 \$peekAhead of method ipl\\Orm\\Query\:\:peekAhead\(\) expects bool, null given\.$#' identifier: argument.type count: 1 path: application/controllers/HostgroupsController.php - message: '#^Cannot access property \$comments_total on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: application/controllers/HostsController.php - message: '#^Cannot access property \$downtimes_total on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: application/controllers/HostsController.php - message: '#^Cannot call method first\(\) on ipl\\Orm\\Query\|null\.$#' identifier: method.nonObject count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:acknowledgeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:addCommentAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:assertIsGrantedOnCommandTargets\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:checkNowAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:completeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:detailsAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:getFeatureStatus\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:processCheckresultAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:removeAcknowledgementAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:scheduleCheckAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:scheduleDowntimeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:searchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:sendCustomNotificationAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:toggleFeaturesAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/HostsController.php - message: '#^Parameter \#1 \$form of method Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:handleCommandForm\(\) expects Icinga\\Module\\Icingadb\\Forms\\Command\\CommandForm\|string, Icinga\\Module\\Icingadb\\Forms\\Command\\Object\\ToggleObjectFeaturesForm\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/HostsController.php - message: '#^Parameter \#1 \$peekAhead of method ipl\\Orm\\Query\:\:peekAhead\(\) expects bool, null given\.$#' identifier: argument.type count: 1 path: application/controllers/HostsController.php - message: '#^Parameter \#1 \$query of method Icinga\\Module\\Icingadb\\Web\\Controller\:\:filter\(\) expects ipl\\Orm\\Query, ipl\\Orm\\Query\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/HostsController.php - message: '#^Parameter \#2 \$summary of class Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectsDetail constructor expects Icinga\\Module\\Icingadb\\Model\\HoststateSummary\|Icinga\\Module\\Icingadb\\Model\\ServicestateSummary, ipl\\Orm\\Model\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/HostsController.php - message: '#^Parameter \#2 \.\.\.\$queries of method Icinga\\Module\\Icingadb\\Web\\Controller\:\:export\(\) expects ipl\\Orm\\Query, ipl\\Orm\\Query\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/HostsController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\HostsController\:\:\$commandTargetModel \(ipl\\Orm\\Model\) in isset\(\) is not nullable\.$#' identifier: isset.property count: 1 path: application/controllers/HostsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\NotificationsController\:\:completeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/NotificationsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\NotificationsController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/NotificationsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\NotificationsController\:\:searchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/NotificationsController.php - message: '#^Parameter \#2 \$default of method Icinga\\Web\\UrlParams\:\:shift\(\) expects string\|null, int\<1, max\> given\.$#' identifier: argument.type count: 1 path: application/controllers/NotificationsController.php - message: '#^Parameter \#2 \$value of method Icinga\\Web\\Url\:\:setParam\(\) expects array\|bool\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/NotificationsController.php - message: '#^Parameter \#2 \$value of static method ipl\\Stdlib\\Filter\:\:lessThanOrEqual\(\) expects float\|int\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/NotificationsController.php - message: '#^Cannot access property \$display_name on mixed\.$#' identifier: property.nonObject count: 1 path: application/controllers/ServiceController.php - message: '#^Cannot access property \$is_overdue on mixed\.$#' identifier: property.nonObject count: 3 path: application/controllers/ServiceController.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 1 path: application/controllers/ServiceController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:acknowledgeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServiceController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:addCommentAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServiceController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:assertIsGrantedOnCommandTargets\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServiceController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:checkNowAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServiceController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:processCheckresultAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServiceController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:removeAcknowledgementAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServiceController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:scheduleCheckAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServiceController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:scheduleDowntimeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServiceController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:sendCustomNotificationAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServiceController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:toggleFeaturesAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServiceController.php - message: '#^Parameter \#1 \$array of function reset expects array\|object, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/ServiceController.php - message: '#^Parameter \#1 \$form of method Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:handleCommandForm\(\) expects Icinga\\Module\\Icingadb\\Forms\\Command\\CommandForm\|string, Icinga\\Module\\Icingadb\\Forms\\Command\\Object\\ToggleObjectFeaturesForm\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/ServiceController.php - message: '#^Parameter \#2 \$apiResult of class Icinga\\Module\\Icingadb\\Widget\\Detail\\ServiceInspectionDetail constructor expects array, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/ServiceController.php - message: '#^Parameter \#2 \$default of method Icinga\\Web\\UrlParams\:\:shift\(\) expects string\|null, int\<1, max\> given\.$#' identifier: argument.type count: 1 path: application/controllers/ServiceController.php - message: '#^Parameter \#2 \$host of static method Icinga\\Module\\Icingadb\\Common\\Links\:\:service\(\) expects Icinga\\Module\\Icingadb\\Model\\Host, mixed given\.$#' identifier: argument.type count: 2 path: application/controllers/ServiceController.php - message: '#^Parameter \#2 \$host of static method Icinga\\Module\\Icingadb\\Common\\Links\:\:serviceSource\(\) expects Icinga\\Module\\Icingadb\\Model\\Host, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/ServiceController.php - message: '#^Parameter \#2 \$host of static method Icinga\\Module\\Icingadb\\Common\\ServiceLinks\:\:history\(\) expects Icinga\\Module\\Icingadb\\Model\\Host, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/ServiceController.php - message: '#^Parameter \#2 \$value of method Icinga\\Web\\Url\:\:setParam\(\) expects array\|bool\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/ServiceController.php - message: '#^Parameter \#2 \$value of static method ipl\\Stdlib\\Filter\:\:lessThanOrEqual\(\) expects float\|int\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/ServiceController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\ServiceController\:\:\$commandTargetModel \(ipl\\Orm\\Model\) in isset\(\) is not nullable\.$#' identifier: isset.property count: 1 path: application/controllers/ServiceController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\ServicegroupController\:\:\$servicegroupName \(string\) does not accept mixed\.$#' identifier: assign.propertyType count: 1 path: application/controllers/ServicegroupController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicegroupsController\:\:completeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicegroupsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicegroupsController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicegroupsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicegroupsController\:\:searchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicegroupsController.php - message: '#^Parameter \#1 \$peekAhead of method ipl\\Orm\\Query\:\:peekAhead\(\) expects bool, null given\.$#' identifier: argument.type count: 1 path: application/controllers/ServicegroupsController.php - message: '#^Call to an undefined method Zend_Controller_Action_Helper_Abstract\:\:getResponseSegment\(\)\.$#' identifier: method.notFound count: 1 path: application/controllers/ServicesController.php - message: '#^Call to an undefined method Zend_Controller_Action_Helper_Abstract\:\:setNoRender\(\)\.$#' identifier: method.notFound count: 1 path: application/controllers/ServicesController.php - message: '#^Call to an undefined method Zend_Controller_Action_Helper_Abstract\:\:setScriptAction\(\)\.$#' identifier: method.notFound count: 1 path: application/controllers/ServicesController.php - message: '#^Call to an undefined method ipl\\Html\\Contract\\FormElement\:\:isChecked\(\)\.$#' identifier: method.notFound count: 1 path: application/controllers/ServicesController.php - message: '#^Cannot access property \$comments_total on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: application/controllers/ServicesController.php - message: '#^Cannot access property \$downtimes_total on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: application/controllers/ServicesController.php - message: '#^Cannot call method first\(\) on ipl\\Orm\\Query\|null\.$#' identifier: method.nonObject count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:acknowledgeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:addCommentAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:assertIsGrantedOnCommandTargets\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:checkNowAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:completeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:detailsAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:getFeatureStatus\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:gridAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:gridSearchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:prepareSearchFilter\(\) has parameter \$additionalColumns with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:processCheckresultAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:removeAcknowledgementAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:scheduleCheckAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:scheduleDowntimeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:searchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:sendCustomNotificationAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:toggleFeaturesAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/ServicesController.php - message: '#^Parameter \#1 \$form of method Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:handleCommandForm\(\) expects Icinga\\Module\\Icingadb\\Forms\\Command\\CommandForm\|string, Icinga\\Module\\Icingadb\\Forms\\Command\\Object\\ToggleObjectFeaturesForm\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/ServicesController.php - message: '#^Parameter \#1 \$peekAhead of method ipl\\Orm\\Query\:\:peekAhead\(\) expects bool, null given\.$#' identifier: argument.type count: 1 path: application/controllers/ServicesController.php - message: '#^Parameter \#1 \$query of method Icinga\\Module\\Icingadb\\Web\\Controller\:\:filter\(\) expects ipl\\Orm\\Query, ipl\\Orm\\Query\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/ServicesController.php - message: '#^Parameter \#2 \$default of method Icinga\\Web\\UrlParams\:\:shift\(\) expects string\|null, false given\.$#' identifier: argument.type count: 2 path: application/controllers/ServicesController.php - message: '#^Parameter \#2 \$summary of class Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectsDetail constructor expects Icinga\\Module\\Icingadb\\Model\\HoststateSummary\|Icinga\\Module\\Icingadb\\Model\\ServicestateSummary, ipl\\Orm\\Model\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/ServicesController.php - message: '#^Parameter \#2 \.\.\.\$queries of method Icinga\\Module\\Icingadb\\Web\\Controller\:\:export\(\) expects ipl\\Orm\\Query, ipl\\Orm\\Query\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/ServicesController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\ServicesController\:\:\$commandTargetModel \(ipl\\Orm\\Model\) in isset\(\) is not nullable\.$#' identifier: isset.property count: 1 path: application/controllers/ServicesController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\TacticalController\:\:completeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/TacticalController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\TacticalController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/TacticalController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\TacticalController\:\:searchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/TacticalController.php - message: '#^Parameter \#1 \$summary of class Icinga\\Module\\Icingadb\\Widget\\HostSummaryDonut constructor expects Icinga\\Module\\Icingadb\\Model\\HoststateSummary, ipl\\Orm\\Model\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/TacticalController.php - message: '#^Parameter \#1 \$summary of class Icinga\\Module\\Icingadb\\Widget\\ServiceSummaryDonut constructor expects Icinga\\Module\\Icingadb\\Model\\ServicestateSummary, ipl\\Orm\\Model\|null given\.$#' identifier: argument.type count: 1 path: application/controllers/TacticalController.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\ApiTransportForm\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/forms/ApiTransportForm.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/Command/CommandForm.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/Command/CommandForm.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Command/CommandForm.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: application/forms/Command/CommandForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\Command\\CommandForm\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/forms/Command/CommandForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\Command\\CommandForm\:\:onSuccess\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/forms/Command/CommandForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\Command\\CommandForm\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: application/forms/Command/CommandForm.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: application/forms/Command/CommandForm.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Forms\\Command\\CommandForm\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: application/forms/Command/CommandForm.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Forms\\Command\\CommandForm\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: application/forms/Command/CommandForm.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: application/forms/Command/CommandForm.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: application/forms/Command/CommandForm.php - message: '#^Call to an undefined method ipl\\Html\\Contract\\FormElement\:\:isChecked\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/Command/Instance/ToggleInstanceFeaturesForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\Command\\Instance\\ToggleInstanceFeaturesForm\:\:__construct\(\) has parameter \$featureStatus with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: application/forms/Command/Instance/ToggleInstanceFeaturesForm.php - message: '#^Property Icinga\\Module\\Icingadb\\Forms\\Command\\Instance\\ToggleInstanceFeaturesForm\:\:\$featureStatus has no type specified\.$#' identifier: missingType.property count: 1 path: application/forms/Command/Instance/ToggleInstanceFeaturesForm.php - message: '#^Property Icinga\\Module\\Icingadb\\Forms\\Command\\Instance\\ToggleInstanceFeaturesForm\:\:\$features has no type specified\.$#' identifier: missingType.property count: 1 path: application/forms/Command/Instance/ToggleInstanceFeaturesForm.php - message: '#^Call to an undefined method ipl\\Html\\Contract\\FormElement\:\:isChecked\(\)\.$#' identifier: method.notFound count: 4 path: application/forms/Command/Object/AcknowledgeProblemForm.php - message: '#^Cannot call method getUsername\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Command/Object/AcknowledgeProblemForm.php - message: '#^Parameter \#1 \$comment of method Icinga\\Module\\Icingadb\\Command\\Object\\WithCommentCommand\:\:setComment\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/Command/Object/AcknowledgeProblemForm.php - message: '#^Parameter \#1 \$duration of class DateInterval constructor expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/Command/Object/AcknowledgeProblemForm.php - message: '#^Call to an undefined method ipl\\Html\\Contract\\FormElement\:\:isChecked\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/Command/Object/AddCommentForm.php - message: '#^Cannot call method getUsername\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Command/Object/AddCommentForm.php - message: '#^Parameter \#1 \$comment of method Icinga\\Module\\Icingadb\\Command\\Object\\WithCommentCommand\:\:setComment\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/Command/Object/AddCommentForm.php - message: '#^Parameter \#1 \$duration of class DateInterval constructor expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/Command/Object/AddCommentForm.php - message: '#^Cannot call method getUsername\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Command/Object/DeleteCommentForm.php - message: '#^Cannot call method getUsername\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Command/Object/DeleteDowntimeForm.php - message: '#^Parameter \#1 \$output of method Icinga\\Module\\Icingadb\\Command\\Object\\ProcessCheckResultCommand\:\:setOutput\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/Command/Object/ProcessCheckResultForm.php - message: '#^Parameter \#1 \$performanceData of method Icinga\\Module\\Icingadb\\Command\\Object\\ProcessCheckResultCommand\:\:setPerformanceData\(\) expects string\|null, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/Command/Object/ProcessCheckResultForm.php - message: '#^Parameter \#1 \$status of method Icinga\\Module\\Icingadb\\Command\\Object\\ProcessCheckResultCommand\:\:setStatus\(\) expects int, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/Command/Object/ProcessCheckResultForm.php - message: '#^Cannot call method getUsername\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Command/Object/RemoveAcknowledgementForm.php - message: '#^Call to an undefined method ipl\\Html\\Contract\\FormElement\:\:isChecked\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/Command/Object/ScheduleCheckForm.php - message: '#^Cannot call method getTimestamp\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: application/forms/Command/Object/ScheduleCheckForm.php - message: '#^Call to an undefined method ipl\\Html\\Contract\\FormElement\:\:isChecked\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/Command/Object/ScheduleHostDowntimeForm.php - message: '#^Parameter \#1 \$duration of class DateInterval constructor expects string, mixed given\.$#' identifier: argument.type count: 3 path: application/forms/Command/Object/ScheduleHostDowntimeForm.php - message: '#^Call to an undefined method ipl\\Html\\Contract\\FormElement\:\:isChecked\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/Command/Object/ScheduleServiceDowntimeForm.php - message: '#^Cannot call method getTimestamp\(\) on mixed\.$#' identifier: method.nonObject count: 2 path: application/forms/Command/Object/ScheduleServiceDowntimeForm.php - message: '#^Cannot call method getUsername\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Command/Object/ScheduleServiceDowntimeForm.php - message: '#^Cannot call method on\(\) on ipl\\Html\\Contract\\Wrappable\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Command/Object/ScheduleServiceDowntimeForm.php - message: '#^Parameter \#1 \$comment of method Icinga\\Module\\Icingadb\\Command\\Object\\WithCommentCommand\:\:setComment\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/Command/Object/ScheduleServiceDowntimeForm.php - message: '#^Parameter \#1 \$duration of class DateInterval constructor expects string, mixed given\.$#' identifier: argument.type count: 3 path: application/forms/Command/Object/ScheduleServiceDowntimeForm.php - message: '#^Property Icinga\\Module\\Icingadb\\Forms\\Command\\Object\\ScheduleServiceDowntimeForm\:\:\$flexibleEnd has no type specified\.$#' identifier: missingType.property count: 1 path: application/forms/Command/Object/ScheduleServiceDowntimeForm.php - message: '#^Call to an undefined method ipl\\Html\\Contract\\FormElement\:\:isChecked\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/Command/Object/SendCustomNotificationForm.php - message: '#^Cannot call method getUsername\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Command/Object/SendCustomNotificationForm.php - message: '#^Parameter \#1 \$comment of method Icinga\\Module\\Icingadb\\Command\\Object\\WithCommentCommand\:\:setComment\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/Command/Object/SendCustomNotificationForm.php - message: '#^Cannot call method getAttributes\(\) on ipl\\Html\\Contract\\Wrappable\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Command/Object/ToggleObjectFeaturesForm.php - message: '#^Cannot cast mixed to int\.$#' identifier: cast.int count: 2 path: application/forms/Command/Object/ToggleObjectFeaturesForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\Command\\Object\\ToggleObjectFeaturesForm\:\:__construct\(\) has parameter \$featureStatus with no type specified\.$#' identifier: missingType.parameter count: 1 path: application/forms/Command/Object/ToggleObjectFeaturesForm.php - message: '#^Parameter \#1 \$enabled of method Icinga\\Module\\Icingadb\\Command\\Object\\ToggleObjectFeatureCommand\:\:setEnabled\(\) expects bool, int given\.$#' identifier: argument.type count: 1 path: application/forms/Command/Object/ToggleObjectFeaturesForm.php - message: '#^Property Icinga\\Module\\Icingadb\\Forms\\Command\\Object\\ToggleObjectFeaturesForm\:\:\$featureStatus has no type specified\.$#' identifier: missingType.property count: 1 path: application/forms/Command/Object/ToggleObjectFeaturesForm.php - message: '#^Property Icinga\\Module\\Icingadb\\Forms\\Command\\Object\\ToggleObjectFeaturesForm\:\:\$features has no type specified\.$#' identifier: missingType.property count: 1 path: application/forms/Command/Object/ToggleObjectFeaturesForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\DatabaseConfigForm\:\:createElements\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/forms/DatabaseConfigForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\DatabaseConfigForm\:\:createElements\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: application/forms/DatabaseConfigForm.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/Navigation/ActionForm.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/Navigation/ActionForm.php - message: '#^Cannot call method addError\(\) on Zend_Form_Element\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Navigation/ActionForm.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/Navigation/ActionForm.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: application/forms/Navigation/ActionForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\Navigation\\ActionForm\:\:createElements\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/forms/Navigation/ActionForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\Navigation\\ActionForm\:\:createElements\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: application/forms/Navigation/ActionForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\Navigation\\ActionForm\:\:isValid\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: application/forms/Navigation/ActionForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\Navigation\\ActionForm\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: application/forms/Navigation/ActionForm.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: application/forms/Navigation/ActionForm.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Forms\\Navigation\\ActionForm\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: application/forms/Navigation/ActionForm.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Forms\\Navigation\\ActionForm\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: application/forms/Navigation/ActionForm.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Forms\\Navigation\\ActionForm\:\:parseRestriction\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/Navigation/ActionForm.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: application/forms/Navigation/ActionForm.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: application/forms/Navigation/ActionForm.php - message: '#^Call to an undefined method Zend_Form_Element\:\:isChecked\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/RedisConfigForm.php - message: '#^Call to an undefined method object\:\:getMessages\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/RedisConfigForm.php - message: '#^Call to an undefined method object\:\:isValid\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/RedisConfigForm.php - message: '#^Cannot call method addError\(\) on Zend_Form_Element\|null\.$#' identifier: method.nonObject count: 3 path: application/forms/RedisConfigForm.php - message: '#^Cannot call method getElements\(\) on Zend_Form_DisplayGroup\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/RedisConfigForm.php - message: '#^Cannot call method getValidator\(\) on Zend_Form_Element\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/RedisConfigForm.php - message: '#^Cannot call method getValue\(\) on Zend_Form_Element\|null\.$#' identifier: method.nonObject count: 3 path: application/forms/RedisConfigForm.php - message: '#^Cannot call method isChecked\(\) on Zend_Form_Element\|null\.$#' identifier: method.nonObject count: 5 path: application/forms/RedisConfigForm.php - message: '#^Cannot call method setDecorators\(\) on Zend_Form_Element\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/RedisConfigForm.php - message: '#^Cannot call method setElements\(\) on Zend_Form_DisplayGroup\|null\.$#' identifier: method.nonObject count: 1 path: application/forms/RedisConfigForm.php - message: '#^Cannot call method setValue\(\) on Zend_Form_Element\|null\.$#' identifier: method.nonObject count: 11 path: application/forms/RedisConfigForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\RedisConfigForm\:\:addInsecureCheckboxIfTls\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/forms/RedisConfigForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\RedisConfigForm\:\:addSkipValidationCheckbox\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/forms/RedisConfigForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\RedisConfigForm\:\:createElements\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/forms/RedisConfigForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\RedisConfigForm\:\:createElements\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: application/forms/RedisConfigForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\RedisConfigForm\:\:isValid\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: application/forms/RedisConfigForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\RedisConfigForm\:\:isValidPartial\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: application/forms/RedisConfigForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\RedisConfigForm\:\:onRequest\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/forms/RedisConfigForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\RedisConfigForm\:\:wrapIplValidator\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: application/forms/RedisConfigForm.php - message: '#^Parameter \#1 \$filename of function file_exists expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/RedisConfigForm.php - message: '#^Parameter \#1 \$filename of function file_get_contents expects string, mixed given\.$#' identifier: argument.type count: 2 path: application/forms/RedisConfigForm.php - message: '#^Parameter \#1 \$path of function basename expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/RedisConfigForm.php - message: '#^Parameter \#1 \$string of function md5 expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/RedisConfigForm.php - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: application/forms/RedisConfigForm.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Cannot access property \$id on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 3 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Method Icinga\\Module\\Icingadb\\Authentication\\ObjectAuthorization\:\:checkGrants\(\) has parameter \$roles with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Method Icinga\\Module\\Icingadb\\Authentication\\ObjectAuthorization\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Parameter \#1 \$column of static method ipl\\Stdlib\\Filter\:\:equal\(\) expects string, array\\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Authentication\\ObjectAuthorization\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Authentication\\ObjectAuthorization\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 6 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Property Icinga\\Module\\Icingadb\\Authentication\\ObjectAuthorization\:\:\$knownGrants type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Property Icinga\\Module\\Icingadb\\Authentication\\ObjectAuthorization\:\:\$matchedFilters type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Authentication/ObjectAuthorization.php - message: '#^Method Icinga\\Module\\Icingadb\\Command\\IcingaApiCommand\:\:create\(\) has parameter \$data with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Command/IcingaApiCommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Command\\IcingaApiCommand\:\:getData\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Command/IcingaApiCommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Command\\IcingaApiCommand\:\:setData\(\) has parameter \$data with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Command/IcingaApiCommand.php - message: '#^Property Icinga\\Module\\Icingadb\\Command\\IcingaApiCommand\:\:\$data type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Command/IcingaApiCommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Command\\Object\\GetObjectCommand\:\:getAttributes\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Command/Object/GetObjectCommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Command\\Object\\GetObjectCommand\:\:setAttributes\(\) has parameter \$attributes with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Command/Object/GetObjectCommand.php - message: '#^Property Icinga\\Module\\Icingadb\\Command\\Object\\GetObjectCommand\:\:\$attributes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Command/Object/GetObjectCommand.php - message: '#^Property Icinga\\Module\\Icingadb\\Command\\Object\\ProcessCheckResultCommand\:\:\$performanceData \(string\) does not accept string\|null\.$#' identifier: assign.propertyType count: 1 path: library/Icingadb/Command/Object/ProcessCheckResultCommand.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php - message: '#^Method Icinga\\Module\\Icingadb\\Command\\Renderer\\IcingaApiCommandRenderer\:\:applyFilter\(\) has parameter \$data with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php - message: '#^Parameter \#1 \$object of method Icinga\\Module\\Icingadb\\Command\\Renderer\\IcingaApiCommandRenderer\:\:getObjectPluralType\(\) expects ipl\\Orm\\Model, ipl\\Orm\\Model\|null given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php - message: '#^Cannot access offset ''code'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: library/Icingadb/Command/Transport/ApiCommandTransport.php - message: '#^Cannot access offset ''error'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: library/Icingadb/Command/Transport/ApiCommandTransport.php - message: '#^Cannot access offset ''results'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 2 path: library/Icingadb/Command/Transport/ApiCommandTransport.php - message: '#^Cannot access offset ''status'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 2 path: library/Icingadb/Command/Transport/ApiCommandTransport.php - message: '#^Cannot access offset ''user'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: library/Icingadb/Command/Transport/ApiCommandTransport.php - message: '#^Cannot access offset 0 on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: library/Icingadb/Command/Transport/ApiCommandTransport.php - message: '#^Parameter \#1 \$array of function array_pop expects array, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Command/Transport/ApiCommandTransport.php - message: '#^Property Icinga\\Module\\Icingadb\\Command\\Transport\\CommandTransportConfig\:\:\$configs type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Command/Transport/CommandTransportConfig.php - message: '#^Property Icinga\\Module\\Icingadb\\Command\\Transport\\CommandTransportConfig\:\:\$queryColumns type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Command/Transport/CommandTransportConfig.php - message: '#^Method Icinga\\Module\\Icingadb\\Command\\Transport\\CommandTransportInterface\:\:send\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Command/Transport/CommandTransportInterface.php - message: '#^Access to constant ATTR_INIT_COMMAND on an unknown class PDO\\Mysql\.$#' identifier: class.notFound count: 1 path: library/Icingadb/Common/Backend.php - message: '#^Possibly invalid array key type mixed\.$#' identifier: offsetAccess.invalidOffset count: 1 path: library/Icingadb/Common/Backend.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\IcingaRedis\:\:fetchHostState\(\) has parameter \$columns with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\IcingaRedis\:\:fetchHostState\(\) has parameter \$ids with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\IcingaRedis\:\:fetchServiceState\(\) has parameter \$columns with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\IcingaRedis\:\:fetchServiceState\(\) has parameter \$ids with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\IcingaRedis\:\:fetchState\(\) has parameter \$columns with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\IcingaRedis\:\:fetchState\(\) has parameter \$ids with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\IcingaRedis\:\:getTlsParams\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: '#^Parameter \#2 \.\.\.\$arrays of function array_merge expects array, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\Links\:\:hostgroup\(\) has parameter \$hostgroup with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Common/Links.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\Links\:\:servicegroup\(\) has parameter \$servicegroup with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Common/Links.php - message: '#^Call to an undefined method Predis\\Client\:\:hGet\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Cannot call method columns\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Cannot call method setTimezone\(\) on DateTime\|false\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:__construct\(\) has parameter \$apiResult with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:createAttributes\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:createCustomVariables\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:createLastCheckResult\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:createLastCheckResult\(\) should return array\|null but empty return statement found\.$#' identifier: return.empty count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:createNameValueTable\(\) has parameter \$data with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:createNameValueTable\(\) has parameter \$formatters with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:createRedisInfo\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:createSourceLocation\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:createSourceLocation\(\) should return array\|null but empty return statement found\.$#' identifier: return.empty count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:formatMilliseconds\(\) has parameter \$ms with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:formatSeconds\(\) has parameter \$s with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:formatState\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Parameter \#1 \$array of function array_diff_key expects array, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Parameter \#1 \$content of static method ipl\\Html\\Table\:\:td\(\) expects array\|ipl\\Html\\Html\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Parameter \#1 \$string of function bin2hex expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Property Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:\$attrs type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Property Icinga\\Module\\Icingadb\\Common\\ObjectInspectionDetail\:\:\$joins type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: '#^Parameter \#1 \$rule of method ipl\\Stdlib\\Filter\\Chain\:\:add\(\) expects ipl\\Stdlib\\Filter\\Rule, ipl\\Stdlib\\Filter\\Rule\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Common/StateBadges.php - message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$customvar on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$display_name on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$hostgroup on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 5 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$services_critical_handled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$services_critical_unhandled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$services_ok on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$services_pending on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$services_total on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$services_unknown_handled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$services_unknown_unhandled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$services_warning_handled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot access property \$services_warning_unhandled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot call method execute\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot call method filter\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Compat/CompatHost.php - message: '#^Cannot clone mixed\.$#' identifier: clone.nonObject count: 2 path: library/Icingadb/Compat/CompatHost.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatHost\:\:__get\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatHost\:\:__get\(\) has parameter \$name with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatHost\:\:__isset\(\) has parameter \$name with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatHost\:\:fromModel\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatHost\:\:getName\(\) should return string but returns mixed\.$#' identifier: return.type count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatHost\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Compat\\CompatHost\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Compat/CompatHost.php - message: '#^Parameter \#1 \$flattenedVars of method Icinga\\Module\\Icingadb\\Model\\CustomvarFlat\:\:unFlattenVars\(\) expects Traversable, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Compat\\CompatHost\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Compat/CompatHost.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Compat/CompatHost.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Compat/CompatHost.php - message: '#^Property Icinga\\Module\\Icingadb\\Compat\\CompatHost\:\:\$legacyColumns has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Property Icinga\\Module\\Icingadb\\Compat\\CompatHost\:\:\$rawCustomvars type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Property Icinga\\Module\\Icingadb\\Compat\\CompatHost\:\:\$rawHostCustomvars type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$customvar on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$display_name on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$hostgroup on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 5 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$services_critical_handled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$services_critical_unhandled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$services_ok on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$services_pending on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$services_total on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$services_unknown_handled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$services_unknown_unhandled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$services_warning_handled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot access property \$services_warning_unhandled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot call method execute\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot call method filter\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Compat/CompatService.php - message: '#^Cannot clone mixed\.$#' identifier: clone.nonObject count: 2 path: library/Icingadb/Compat/CompatService.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:__get\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:__get\(\) has parameter \$name with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:__isset\(\) has parameter \$name with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:fetchHost\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:fromModel\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:getName\(\) should return string but returns mixed\.$#' identifier: return.type count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Compat/CompatService.php - message: '#^Parameter \#1 \$flattenedVars of method Icinga\\Module\\Icingadb\\Model\\CustomvarFlat\:\:unFlattenVars\(\) expects Traversable, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Parameter \#1 \$object of class Icinga\\Module\\Icingadb\\Compat\\CompatHost constructor expects ipl\\Orm\\Model, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Compat/CompatService.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Compat/CompatService.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Compat/CompatService.php - message: '#^Property Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:\$legacyColumns has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Property Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:\$rawCustomvars type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Property Icinga\\Module\\Icingadb\\Compat\\CompatService\:\:\$rawHostCustomvars type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:commentsColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:commonColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:commonParameters\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:contactgroupsColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:contactsColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:downtimesColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:historyColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:hostColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:hostgroupsColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:hostsColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:hostsParameters\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:multipleHostsColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:multipleServicesColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:notificationHistoryColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:rewrite\(\) has parameter \$legacyColumns with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:serviceColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:servicegridColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:servicegroupsColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:servicesColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:servicesParameters\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:transformParams\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Compat\\UrlMigrator\:\:transformWildcardFilter\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Parameter \#1 \$string of function strtolower expects string, mixed given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Compat/UrlMigrator.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\CsvResultSet\:\:extractKeysAndValues\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Data/CsvResultSet.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\CsvResultSet\:\:formatValue\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Data/CsvResultSet.php - message: '#^Parameter \#1 \$array of function array_keys expects array, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/CsvResultSet.php - message: '#^Parameter \#1 \$key of method Icinga\\Module\\Icingadb\\Data\\CsvResultSet\:\:formatValue\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/CsvResultSet.php - message: '#^Parameter \#1 \$model of method Icinga\\Module\\Icingadb\\Data\\CsvResultSet\:\:extractKeysAndValues\(\) expects ipl\\Orm\\Model, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/CsvResultSet.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\JsonResultSet\:\:createObject\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Data/JsonResultSet.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\JsonResultSet\:\:formatValue\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Data/JsonResultSet.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\JsonResultSet\:\:formatValue\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Data/JsonResultSet.php - message: '#^Parameter \#1 \$key of method Icinga\\Module\\Icingadb\\Data\\JsonResultSet\:\:formatValue\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/JsonResultSet.php - message: '#^Parameter \#1 \$model of method Icinga\\Module\\Icingadb\\Data\\JsonResultSet\:\:createObject\(\) expects ipl\\Orm\\Model, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/JsonResultSet.php - message: '#^Argument of an invalid type array\|null supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable count: 1 path: library/Icingadb/Data/PivotTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\PivotTable\:\:__construct\(\) has parameter \$gridcols with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Data/PivotTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\PivotTable\:\:toArray\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Data/PivotTable.php - message: '#^Parameter \#1 \$haystack of function strpos expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/PivotTable.php - message: '#^Parameter \#2 \$string of function explode expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/PivotTable.php - message: '#^Property Icinga\\Module\\Icingadb\\Data\\PivotTable\:\:\$gridcols type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Data/PivotTable.php - message: '#^Property Icinga\\Module\\Icingadb\\Data\\PivotTable\:\:\$order type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Data/PivotTable.php - message: '#^Property Icinga\\Module\\Icingadb\\Data\\PivotTable\:\:\$xAxisFilter \(ipl\\Stdlib\\Filter\\Rule\) does not accept ipl\\Stdlib\\Filter\\Rule\|null\.$#' identifier: assign.propertyType count: 1 path: library/Icingadb/Data/PivotTable.php - message: '#^Property Icinga\\Module\\Icingadb\\Data\\PivotTable\:\:\$yAxisFilter \(ipl\\Stdlib\\Filter\\Rule\) does not accept ipl\\Stdlib\\Filter\\Rule\|null\.$#' identifier: assign.propertyType count: 1 path: library/Icingadb/Data/PivotTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\VolatileCsvResults\:\:extractKeysAndValues\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Data/VolatileCsvResults.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\VolatileCsvResults\:\:formatValue\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Data/VolatileCsvResults.php - message: '#^Parameter \#1 \$array of function array_keys expects array, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/VolatileCsvResults.php - message: '#^Parameter \#1 \$key of method Icinga\\Module\\Icingadb\\Data\\VolatileCsvResults\:\:formatValue\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/VolatileCsvResults.php - message: '#^Parameter \#1 \$model of method Icinga\\Module\\Icingadb\\Data\\VolatileCsvResults\:\:extractKeysAndValues\(\) expects ipl\\Orm\\Model, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/VolatileCsvResults.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\VolatileJsonResults\:\:createObject\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Data/VolatileJsonResults.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\VolatileJsonResults\:\:formatValue\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Data/VolatileJsonResults.php - message: '#^Method Icinga\\Module\\Icingadb\\Data\\VolatileJsonResults\:\:formatValue\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Data/VolatileJsonResults.php - message: '#^Parameter \#1 \$key of method Icinga\\Module\\Icingadb\\Data\\VolatileJsonResults\:\:formatValue\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/VolatileJsonResults.php - message: '#^Parameter \#1 \$model of method Icinga\\Module\\Icingadb\\Data\\VolatileJsonResults\:\:createObject\(\) expects ipl\\Orm\\Model, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Data/VolatileJsonResults.php - message: '#^Method Icinga\\Module\\Icingadb\\Hook\\ActionsHook\\ObjectActionsHook\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Hook/ActionsHook/ObjectActionsHook.php - message: '#^Method Icinga\\Module\\Icingadb\\Hook\\CustomVarRendererHook\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Hook/CustomVarRendererHook.php - message: '#^Method Icinga\\Module\\Icingadb\\Hook\\ExtensionHook\\BaseExtensionHook\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Hook/ExtensionHook/BaseExtensionHook.php - message: '#^Method Icinga\\Module\\Icingadb\\Hook\\ExtensionHook\\BaseExtensionHook\:\:injectExtensions\(\) has parameter \$coreElements with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Hook/ExtensionHook/BaseExtensionHook.php - message: '#^Method Icinga\\Module\\Icingadb\\Hook\\ExtensionHook\\BaseExtensionHook\:\:injectExtensions\(\) has parameter \$extensions with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Hook/ExtensionHook/BaseExtensionHook.php - message: '#^Method Icinga\\Module\\Icingadb\\Hook\\ExtensionHook\\BaseExtensionHook\:\:injectExtensions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Hook/ExtensionHook/BaseExtensionHook.php - message: '#^Cannot cast ipl\\Html\\ValidHtml to string\.$#' identifier: cast.string count: 1 path: library/Icingadb/Hook/ExtensionHook/ObjectsDetailExtensionHook.php - message: '#^Method Icinga\\Module\\Icingadb\\Hook\\IcingadbSupportHook\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Hook/IcingadbSupportHook.php - message: '#^Method Icinga\\Module\\Icingadb\\Hook\\PluginOutputHook\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Hook/PluginOutputHook.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Hook/TabHook.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Hook/TabHook.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Hook/TabHook.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Hook/TabHook.php - message: '#^Method Icinga\\Module\\Icingadb\\Hook\\TabHook\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Hook/TabHook.php - message: '#^Method Icinga\\Module\\Icingadb\\Hook\\TabHook\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Hook/TabHook.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Hook/TabHook.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Hook\\TabHook\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Hook/TabHook.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Hook\\TabHook\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Hook/TabHook.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Hook/TabHook.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Hook/TabHook.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\AcknowledgementHistory\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/AcknowledgementHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\AcknowledgementHistory\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/AcknowledgementHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\AcknowledgementHistory\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/AcknowledgementHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ActionUrl\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ActionUrl.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ActionUrl\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ActionUrl.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ActionUrl\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/ActionUrl.php - message: '#^Parameter \#1 \$haystack of function strpos expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Model/Behavior/ActionAndNoteUrl.php - message: '#^Parameter \#2 \$string of function explode expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Model/Behavior/ActionAndNoteUrl.php - message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable count: 1 path: library/Icingadb/Model/Behavior/Bitmask.php - message: '#^Cannot access offset mixed on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 2 path: library/Icingadb/Model/Behavior/Bitmask.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Behavior\\Bitmask\:\:rewriteCondition\(\) should return ipl\\Stdlib\\Filter\\Rule\|null but empty return statement found\.$#' identifier: return.empty count: 2 path: library/Icingadb/Model/Behavior/Bitmask.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Behavior\\FlattenedObjectVars\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Model\\Behavior\\FlattenedObjectVars\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Model\\Behavior\\FlattenedObjectVars\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Parameter \#1 \$string of function substr expects string, string\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Parameter \#2 \$targetPath of method ipl\\Orm\\Query\:\:createSubQuery\(\) expects string, string\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Part \$column \(mixed\) of encapsed string cannot be cast to string\.$#' identifier: encapsedStringPart.nonString count: 1 path: library/Icingadb/Model/Behavior/FlattenedObjectVars.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Behavior\\ReRoute\:\:__construct\(\) has parameter \$routes with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Behavior/ReRoute.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Behavior\\ReRoute\:\:getRoutes\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Behavior/ReRoute.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Behavior\\ReRoute\:\:rewriteCondition\(\) should return ipl\\Stdlib\\Filter\\Rule\|null but empty return statement found\.$#' identifier: return.empty count: 1 path: library/Icingadb/Model/Behavior/ReRoute.php - message: '#^Parameter \#1 \$haystack of function strpos expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Model/Behavior/ReRoute.php - message: '#^Parameter \#1 \$path of method Icinga\\Module\\Icingadb\\Model\\Behavior\\ReRoute\:\:rewritePath\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Model/Behavior/ReRoute.php - message: '#^Parameter \#1 \$string of function substr expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Model/Behavior/ReRoute.php - message: '#^Parameter \#1 \$string of function substr expects string, string\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Model/Behavior/ReRoute.php - message: '#^Property Icinga\\Module\\Icingadb\\Model\\Behavior\\ReRoute\:\:\$routes has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Model/Behavior/ReRoute.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Checkcommand\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Checkcommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Checkcommand\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Checkcommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Checkcommand\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Checkcommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CheckcommandArgument\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/CheckcommandArgument.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CheckcommandArgument\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/CheckcommandArgument.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CheckcommandArgument\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/CheckcommandArgument.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CheckcommandCustomvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/CheckcommandCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CheckcommandCustomvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/CheckcommandCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CheckcommandEnvvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/CheckcommandEnvvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CheckcommandEnvvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/CheckcommandEnvvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CheckcommandEnvvar\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/CheckcommandEnvvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Comment\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Comment.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Comment\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Comment.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Comment\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Comment.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Comment\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Comment.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Comment\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Comment.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CommentHistory\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/CommentHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CommentHistory\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/CommentHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CommentHistory\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/CommentHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Customvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Customvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Customvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Customvar.php - message: '#^Access to an undefined property object\:\:\$flatname\.$#' identifier: property.notFound count: 1 path: library/Icingadb/Model/CustomvarFlat.php - message: '#^Access to an undefined property object\:\:\$value\.$#' identifier: property.notFound count: 3 path: library/Icingadb/Model/CustomvarFlat.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CustomvarFlat\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/CustomvarFlat.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CustomvarFlat\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/CustomvarFlat.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CustomvarFlat\:\:unFlattenVars\(\) has parameter \$flattenedVars with no value type specified in iterable type Traversable\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/CustomvarFlat.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\CustomvarFlat\:\:unFlattenVars\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/CustomvarFlat.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Downtime\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Downtime.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Downtime\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Downtime.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Downtime\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Downtime.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Downtime\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Downtime.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Downtime\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Downtime.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\DowntimeHistory\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/DowntimeHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\DowntimeHistory\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/DowntimeHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\DowntimeHistory\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/DowntimeHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Endpoint\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Endpoint.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Endpoint\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Endpoint.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Endpoint\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Endpoint.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Environment\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Environment.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Environment\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Environment.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Environment\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Environment.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Eventcommand\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Eventcommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Eventcommand\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Eventcommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Eventcommand\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Eventcommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\EventcommandArgument\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/EventcommandArgument.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\EventcommandArgument\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/EventcommandArgument.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\EventcommandArgument\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/EventcommandArgument.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\EventcommandCustomvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/EventcommandCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\EventcommandCustomvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/EventcommandCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\EventcommandEnvvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/EventcommandEnvvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\EventcommandEnvvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/EventcommandEnvvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\EventcommandEnvvar\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/EventcommandEnvvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\FlappingHistory\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/FlappingHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\FlappingHistory\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/FlappingHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\FlappingHistory\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/FlappingHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\History\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/History.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\History\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/History.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\History\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/History.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\History\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/History.php - message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable count: 2 path: library/Icingadb/Model/Host.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Model/Host.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Model/Host.php - message: '#^Cannot access property \$flatname on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Model/Host.php - message: '#^Cannot access property \$flatvalue on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Model/Host.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Model/Host.php - message: '#^Cannot access property \$value on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Model/Host.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Model/Host.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Model/Host.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Host\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Host.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Host\:\:createDefaults\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Host.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Host\:\:createDefaults\(\) has parameter \$defaults with no value type specified in iterable type ipl\\Orm\\Defaults\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Host.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Host\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Host.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Host\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Host.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Host\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Host.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Host\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Host.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Host\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Model/Host.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Model/Host.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Model\\Host\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Model/Host.php - message: '#^Parameter \#1 \$query of method Icinga\\Module\\Icingadb\\Model\\Host\:\:applyRestrictions\(\) expects ipl\\Orm\\Query, mixed given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Model/Host.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Model\\Host\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Model/Host.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Model/Host.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Model/Host.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\HostCustomvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/HostCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\HostCustomvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/HostCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\HostState\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/HostState.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\HostState\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/HostState.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Hostgroup\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Hostgroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Hostgroup\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Hostgroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Hostgroup\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Hostgroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Hostgroup\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Hostgroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Hostgroup\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Hostgroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\HostgroupCustomvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/HostgroupCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\HostgroupCustomvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/HostgroupCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\HostgroupMember\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/HostgroupMember.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\HostgroupMember\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/HostgroupMember.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Hostgroupsummary\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Hostgroupsummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Hostgroupsummary\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Hostgroupsummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Hostgroupsummary\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Hostgroupsummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Hostgroupsummary\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Hostgroupsummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Hostgroupsummary\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Hostgroupsummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Hostgroupsummary\:\:getUnions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Hostgroupsummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\HoststateSummary\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/HoststateSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\HoststateSummary\:\:getDefaultSort\(\) should return array\|string but returns null\.$#' identifier: return.type count: 1 path: library/Icingadb/Model/HoststateSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\HoststateSummary\:\:getSummaryColumns\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/HoststateSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\IconImage\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/IconImage.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\IconImage\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/IconImage.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\IconImage\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/IconImage.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Instance\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Instance.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Instance\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Instance.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Instance\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Instance.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotesUrl\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotesUrl.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotesUrl\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotesUrl.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotesUrl\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/NotesUrl.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Notification\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Notification.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Notification\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Notification.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Notification\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Notification.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationCustomvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationCustomvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationHistory\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationHistory\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationHistory\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/NotificationHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationHistory\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/NotificationHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationHistory\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/NotificationHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationUser\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationUser.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationUser\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationUser.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationUsergroup\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationUsergroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationUsergroup\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationUsergroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Notificationcommand\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Notificationcommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Notificationcommand\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Notificationcommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Notificationcommand\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Notificationcommand.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationcommandArgument\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationcommandArgument.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationcommandArgument\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationcommandArgument.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationcommandArgument\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/NotificationcommandArgument.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationcommandCustomvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationcommandCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationcommandCustomvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationcommandCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationcommandEnvvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationcommandEnvvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationcommandEnvvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/NotificationcommandEnvvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\NotificationcommandEnvvar\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/NotificationcommandEnvvar.php - message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable count: 2 path: library/Icingadb/Model/Service.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Model/Service.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Model/Service.php - message: '#^Cannot access property \$flatname on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Model/Service.php - message: '#^Cannot access property \$flatvalue on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Model/Service.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Model/Service.php - message: '#^Cannot access property \$value on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Model/Service.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Model/Service.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Model/Service.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Service\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Service.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Service\:\:createDefaults\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Service.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Service\:\:createDefaults\(\) has parameter \$defaults with no value type specified in iterable type ipl\\Orm\\Defaults\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Service.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Service\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Service.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Service\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Service.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Service\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Service.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Service\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Service.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Service\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Model/Service.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Model/Service.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Model\\Service\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Model/Service.php - message: '#^Parameter \#1 \$query of method Icinga\\Module\\Icingadb\\Model\\Service\:\:applyRestrictions\(\) expects ipl\\Orm\\Query, mixed given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Model/Service.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Model\\Service\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Model/Service.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Model/Service.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Model/Service.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServiceCustomvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ServiceCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServiceCustomvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ServiceCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServiceState\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ServiceState.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServiceState\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/ServiceState.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Servicegroup\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Servicegroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Servicegroup\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Servicegroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Servicegroup\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Servicegroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Servicegroup\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Servicegroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Servicegroup\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Servicegroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicegroupCustomvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ServicegroupCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicegroupCustomvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ServicegroupCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicegroupMember\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ServicegroupMember.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicegroupMember\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ServicegroupMember.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicegroupSummary\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ServicegroupSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicegroupSummary\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ServicegroupSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicegroupSummary\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/ServicegroupSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicegroupSummary\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/ServicegroupSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicegroupSummary\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/ServicegroupSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicegroupSummary\:\:getUnions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/ServicegroupSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicestateSummary\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/ServicestateSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicestateSummary\:\:getDefaultSort\(\) should return array\|string but returns null\.$#' identifier: return.type count: 1 path: library/Icingadb/Model/ServicestateSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicestateSummary\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/ServicestateSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\ServicestateSummary\:\:getSummaryColumns\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/ServicestateSummary.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\State\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/State.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\StateHistory\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/StateHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\StateHistory\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/StateHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\StateHistory\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/StateHistory.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Timeperiod\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Timeperiod.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Timeperiod\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Timeperiod.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Timeperiod\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Timeperiod.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\TimeperiodCustomvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/TimeperiodCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\TimeperiodCustomvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/TimeperiodCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\TimeperiodOverrideExclude\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/TimeperiodOverrideExclude.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\TimeperiodOverrideExclude\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/TimeperiodOverrideExclude.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\TimeperiodOverrideInclude\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/TimeperiodOverrideInclude.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\TimeperiodOverrideInclude\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/TimeperiodOverrideInclude.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\TimeperiodRange\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/TimeperiodRange.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\TimeperiodRange\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/TimeperiodRange.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\TimeperiodRange\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/TimeperiodRange.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\User\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/User.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\User\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/User.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\User\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/User.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\User\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/User.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\User\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/User.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\UserCustomvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/UserCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\UserCustomvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/UserCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Usergroup\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Usergroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Usergroup\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Usergroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Usergroup\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Usergroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Usergroup\:\:getDefaultSort\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Usergroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Usergroup\:\:getSearchColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Usergroup.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\UsergroupCustomvar\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/UsergroupCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\UsergroupCustomvar\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/UsergroupCustomvar.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\UsergroupMember\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/UsergroupMember.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\UsergroupMember\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/UsergroupMember.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Vars\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Vars.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Zone\:\:createBehaviors\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Zone.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Zone\:\:createRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Model/Zone.php - message: '#^Method Icinga\\Module\\Icingadb\\Model\\Zone\:\:getColumnDefinitions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Model/Zone.php - message: '#^Cannot call method getTimestamp\(\) on mixed\.$#' identifier: method.nonObject count: 3 path: library/Icingadb/ProvidedHook/ApplicationState.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\ApplicationState\:\:collectMessages\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/ProvidedHook/ApplicationState.php - message: '#^Parameter \#2 \$timestamp of method Icinga\\Application\\Hook\\ApplicationStateHook\:\:addError\(\) expects int, mixed given\.$#' identifier: argument.type count: 3 path: library/Icingadb/ProvidedHook/ApplicationState.php - message: '#^Parameter \#1 \$rule of static method ipl\\Web\\Filter\\QueryString\:\:render\(\) expects ipl\\Stdlib\\Filter\\Rule, ipl\\Stdlib\\Filter\\Rule\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/ProvidedHook/CreateHostsSlaReport.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/ProvidedHook/CreateServiceSlaReport.php - message: '#^Parameter \#1 \$rule of static method ipl\\Web\\Filter\\QueryString\:\:render\(\) expects ipl\\Stdlib\\Filter\\Rule, ipl\\Stdlib\\Filter\\Rule\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/ProvidedHook/CreateServicesSlaReport.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/ProvidedHook/IcingaHealth.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\IcingaHealth\:\:getInstance\(\) should return Icinga\\Module\\Icingadb\\Model\\Instance\|null but returns ipl\\Orm\\Model\|null\.$#' identifier: return.type count: 1 path: library/Icingadb/ProvidedHook/IcingaHealth.php - message: '#^Property Icinga\\Module\\Icingadb\\ProvidedHook\\IcingaHealth\:\:\$instance \(Icinga\\Module\\Icingadb\\Model\\Instance\) does not accept ipl\\Orm\\Model\|null\.$#' identifier: assign.propertyType count: 1 path: library/Icingadb/ProvidedHook/IcingaHealth.php - message: '#^Cannot call method getTimestamp\(\) on mixed\.$#' identifier: method.nonObject count: 3 path: library/Icingadb/ProvidedHook/RedisHealth.php - message: '#^Cannot access property \$display_name on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php - message: '#^Cannot access property \$sla on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\HostSlaReport\:\:fetchSla\(\) return type has no value type specified in iterable type iterable\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php - message: '#^Cannot access property \$display_name on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php - message: '#^Cannot access property \$host on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php - message: '#^Cannot access property \$sla on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\ServiceSlaReport\:\:fetchSla\(\) return type has no value type specified in iterable type iterable\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Cannot call method count\(\) on Icinga\\Module\\Reporting\\ReportData\|null\.$#' identifier: method.nonObject count: 2 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Cannot call method format\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Cannot call method getAverages\(\) on Icinga\\Module\\Reporting\\ReportData\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Cannot call method getDimensions\(\) on Icinga\\Module\\Reporting\\ReportData\|null\.$#' identifier: method.nonObject count: 2 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Cannot call method getRows\(\) on Icinga\\Module\\Reporting\\ReportData\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Cannot call method getValues\(\) on Icinga\\Module\\Reporting\\ReportData\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\SlaReport\:\:fetchReportData\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\SlaReport\:\:fetchReportData\(\) has parameter \$config with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\SlaReport\:\:fetchSla\(\) return type has no value type specified in iterable type iterable\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\SlaReport\:\:getData\(\) has parameter \$config with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\SlaReport\:\:getHtml\(\) has parameter \$config with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\SlaReport\:\:initConfigForm\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\SlaReport\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\SlaReport\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\SlaReport\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Parameter \#1 \$start of class Icinga\\Module\\Reporting\\Timerange constructor expects DateTime, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Parameter \#1 \$value of function count expects array\|Countable, Icinga\\Module\\Reporting\\ReportData\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Parameter \#2 \$end of class Icinga\\Module\\Reporting\\Timerange constructor expects DateTime, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Parameter \#2 \$interval of method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\SlaReport\:\:yieldTimerange\(\) expects DateInterval, DateInterval\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: '#^Cannot call method count\(\) on Icinga\\Module\\Reporting\\ReportData\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php - message: '#^Cannot call method getAverages\(\) on Icinga\\Module\\Reporting\\ReportData\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php - message: '#^Cannot call method getDimensions\(\) on Icinga\\Module\\Reporting\\ReportData\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\TotalHostSlaReport\:\:getHtml\(\) has parameter \$config with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php - message: '#^Cannot call method count\(\) on Icinga\\Module\\Reporting\\ReportData\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php - message: '#^Cannot call method getAverages\(\) on Icinga\\Module\\Reporting\\ReportData\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php - message: '#^Cannot call method getDimensions\(\) on Icinga\\Module\\Reporting\\ReportData\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\Reporting\\TotalServiceSlaReport\:\:getHtml\(\) has parameter \$config with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/ProvidedHook/Reporting/TotalServiceSlaReport.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/ProvidedHook/X509/Sni.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/ProvidedHook/X509/Sni.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/ProvidedHook/X509/Sni.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/ProvidedHook/X509/Sni.php - message: '#^Method Icinga\\Module\\Icingadb\\ProvidedHook\\X509\\Sni\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/ProvidedHook/X509/Sni.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/ProvidedHook/X509/Sni.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\ProvidedHook\\X509\\Sni\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/ProvidedHook/X509/Sni.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\ProvidedHook\\X509\\Sni\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/ProvidedHook/X509/Sni.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/ProvidedHook/X509/Sni.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/ProvidedHook/X509/Sni.php - message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' identifier: foreach.nonIterable count: 1 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Cannot access offset mixed on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Cannot access property \$host on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Cannot access property \$id on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Cannot access property \$state on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Cannot call method setProperties\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Method Icinga\\Module\\Icingadb\\Redis\\VolatileStateResults\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Redis\\VolatileStateResults\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Redis\\VolatileStateResults\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Redis/VolatileStateResults.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\ApiTransportPage\:\:addSkipValidationCheckbox\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Setup/ApiTransportPage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\ApiTransportPage\:\:createElements\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Setup/ApiTransportPage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\ApiTransportPage\:\:createElements\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/ApiTransportPage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\ApiTransportPage\:\:isValid\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/ApiTransportPage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\ApiTransportPage\:\:isValidPartial\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/ApiTransportPage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\ApiTransportStep\:\:__construct\(\) has parameter \$data with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/ApiTransportStep.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\ApiTransportStep\:\:getReport\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/ApiTransportStep.php - message: '#^Property Icinga\\Module\\Icingadb\\Setup\\ApiTransportStep\:\:\$data type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/ApiTransportStep.php - message: '#^Cannot call method setMultiOptions\(\) on Zend_Form_Element\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Setup/DbResourcePage.php - message: '#^Cannot call method setValue\(\) on Zend_Form_Element\|null\.$#' identifier: method.nonObject count: 2 path: library/Icingadb/Setup/DbResourcePage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\DbResourcePage\:\:addSkipValidationCheckbox\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Setup/DbResourcePage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\DbResourcePage\:\:createElements\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Setup/DbResourcePage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\DbResourcePage\:\:createElements\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/DbResourcePage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\DbResourcePage\:\:isValid\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/DbResourcePage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\DbResourcePage\:\:isValidPartial\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/DbResourcePage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\DbResourceStep\:\:__construct\(\) has parameter \$data with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/DbResourceStep.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\DbResourceStep\:\:getReport\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/DbResourceStep.php - message: '#^Property Icinga\\Module\\Icingadb\\Setup\\DbResourceStep\:\:\$data type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/DbResourceStep.php - message: '#^Call to an undefined method Icinga\\Web\\Form\:\:setSubjectTitle\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Setup/IcingaDbWizard.php - message: '#^Call to an undefined method Icinga\\Web\\Form\:\:setSummary\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Setup/IcingaDbWizard.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\IcingaDbWizard\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Setup/IcingaDbWizard.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\IcingaDbWizard\:\:setupPage\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Setup/IcingaDbWizard.php - message: '#^Call to an undefined method Zend_Form_Element\:\:isChecked\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Setup/RedisPage.php - message: '#^Cannot call method setIgnore\(\) on Zend_Form_Element\|null\.$#' identifier: method.nonObject count: 3 path: library/Icingadb/Setup/RedisPage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\RedisPage\:\:createElements\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Setup/RedisPage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\RedisPage\:\:createElements\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/RedisPage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\RedisPage\:\:isValid\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/RedisPage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\RedisPage\:\:isValidPartial\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/RedisPage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\RedisStep\:\:__construct\(\) has parameter \$data with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/RedisStep.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\RedisStep\:\:getReport\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/RedisStep.php - message: '#^Property Icinga\\Module\\Icingadb\\Setup\\RedisStep\:\:\$data type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/RedisStep.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\WelcomePage\:\:createElements\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Setup/WelcomePage.php - message: '#^Method Icinga\\Module\\Icingadb\\Setup\\WelcomePage\:\:createElements\(\) has parameter \$formData with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Setup/WelcomePage.php - message: '#^Class Icinga\\Module\\Icingadb\\Util\\FeatureStatus extends generic class ArrayObject but does not specify its types\: TKey, TValue$#' identifier: missingType.generics count: 1 path: library/Icingadb/Util/FeatureStatus.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\FeatureStatus\:\:__construct\(\) has parameter \$summary with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/FeatureStatus.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\FeatureStatus\:\:getFeatureStatus\(\) has parameter \$summary with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/FeatureStatus.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfData\:\:calculatePieChartData\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Util/PerfData.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfData\:\:format\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Util/PerfData.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfData\:\:format\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfData.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfData\:\:parse\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Util/PerfData.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfData\:\:toArray\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Util/PerfData.php - message: '#^Parameter \#1 \$num of function number_format expects float, float\|int\|string given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Util/PerfData.php - message: '#^Parameter \#1 \$value of method Icinga\\Module\\Icingadb\\Util\\ThresholdRange\:\:contains\(\) expects float, float\|null given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Util/PerfData.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:ampereSeconds\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:amperes\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:bits\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:bytes\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:formatForUnits\(\) has parameter \$units with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:grams\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:liters\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:ohms\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:seconds\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:volts\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:wattHours\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:watts\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$ampSecondPrefix has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$amperePrefix has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$bitPrefix has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$bytePrefix has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$generalBase has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$gramPrefix has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$instance has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$literPrefix has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$ohmPrefix has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$secondPrefix has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$voltPrefix has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$wattHourPrefix has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataFormat\:\:\$wattPrefix has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Util/PerfDataFormat.php - message: '#^Class Icinga\\Module\\Icingadb\\Util\\PerfDataSet implements generic interface IteratorAggregate but does not specify its types\: TKey, TValue$#' identifier: missingType.generics count: 1 path: library/Icingadb/Util/PerfDataSet.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataSet\:\:asArray\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Util/PerfDataSet.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataSet\:\:getIterator\(\) return type with generic class ArrayIterator does not specify its types\: TKey, TValue$#' identifier: missingType.generics count: 1 path: library/Icingadb/Util/PerfDataSet.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataSet\:\:parse\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Util/PerfDataSet.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PerfDataSet\:\:skipSpaces\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Util/PerfDataSet.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PerfDataSet\:\:\$perfdata type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Util/PerfDataSet.php - message: '#^Cannot access property \$long_output on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Util/PluginOutput.php - message: '#^Cannot access property \$output on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Util/PluginOutput.php - message: '#^Method Icinga\\Module\\Icingadb\\Util\\PluginOutput\:\:render\(\) should return string but returns string\|null\.$#' identifier: return.type count: 1 path: library/Icingadb/Util/PluginOutput.php - message: '#^Parameter \#1 \$html of static method Icinga\\Web\\Helper\\HtmlPurifier\:\:process\(\) expects string, string\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Util/PluginOutput.php - message: '#^Parameter \#1 \$string of function trim expects string, string\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Util/PluginOutput.php - message: '#^Property Icinga\\Module\\Icingadb\\Util\\PluginOutput\:\:\$renderedOutput \(string\) does not accept string\|null\.$#' identifier: assign.propertyType count: 1 path: library/Icingadb/Util/PluginOutput.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\GridViewModeSwitcher\:\:getTitle\(\) should return string but returns string\|null\.$#' identifier: return.type count: 1 path: library/Icingadb/Web/Control/GridViewModeSwitcher.php - message: '#^Property Icinga\\Module\\Icingadb\\Web\\Control\\GridViewModeSwitcher\:\:\$viewModes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Control/GridViewModeSwitcher.php - message: '#^Call to an undefined method ipl\\Html\\Contract\\FormElement\:\:isChecked\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Control/ProblemToggle.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\ProblemToggle\:\:__construct\(\) has parameter \$filter with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Web/Control/ProblemToggle.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\ProblemToggle\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Control/ProblemToggle.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\ProblemToggle\:\:protectId\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Control/ProblemToggle.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\ProblemToggle\:\:protectId\(\) has parameter \$id with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Web/Control/ProblemToggle.php - message: '#^Property Icinga\\Module\\Icingadb\\Web\\Control\\ProblemToggle\:\:\$filter has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Web/Control/ProblemToggle.php - message: '#^Property Icinga\\Module\\Icingadb\\Web\\Control\\ProblemToggle\:\:\$protector has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Web/Control/ProblemToggle.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Call to an undefined method object\:\:getTableName\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\SearchBar\\ObjectSuggestions\:\:collectRelations\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\SearchBar\\ObjectSuggestions\:\:collectRelations\(\) has parameter \$models with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\SearchBar\\ObjectSuggestions\:\:collectRelations\(\) has parameter \$path with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\SearchBar\\ObjectSuggestions\:\:fetchColumnSuggestions\(\) return type has no value type specified in iterable type Traversable\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\SearchBar\\ObjectSuggestions\:\:fetchValueSuggestions\(\) return type has no value type specified in iterable type Traversable\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\SearchBar\\ObjectSuggestions\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Web\\Control\\SearchBar\\ObjectSuggestions\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Parameter \#1 \$filter of method ipl\\Orm\\Query\:\:filter\(\) expects ipl\\Stdlib\\Filter\\Rule, ipl\\Stdlib\\Filter\\Rule\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Parameter \#1 \$path of method ipl\\Orm\\Resolver\:\:qualifyPath\(\) expects string, mixed given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Web\\Control\\SearchBar\\ObjectSuggestions\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Parameter \#2 \$subject of method ipl\\Orm\\Resolver\:\:resolveRelation\(\) expects ipl\\Orm\\Model\|null, object given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Part \$name \(mixed\) of encapsed string cannot be cast to string\.$#' identifier: encapsedStringPart.nonString count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Property Icinga\\Module\\Icingadb\\Web\\Control\\SearchBar\\ObjectSuggestions\:\:\$customVarSources type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Property Icinga\\Module\\Icingadb\\Web\\Control\\SearchBar\\ObjectSuggestions\:\:\$model \(ipl\\Orm\\Model\) does not accept object\.$#' identifier: assign.propertyType count: 1 path: library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\ViewModeSwitcher\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Control/ViewModeSwitcher.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\ViewModeSwitcher\:\:getTitle\(\) should return string but returns string\|null\.$#' identifier: return.type count: 1 path: library/Icingadb/Web/Control/ViewModeSwitcher.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\ViewModeSwitcher\:\:protectId\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Control/ViewModeSwitcher.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Control\\ViewModeSwitcher\:\:protectId\(\) has parameter \$id with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Web/Control/ViewModeSwitcher.php - message: '#^Parameter \#1 \$key of function array_key_exists expects int\|string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Control/ViewModeSwitcher.php - message: '#^Property Icinga\\Module\\Icingadb\\Web\\Control\\ViewModeSwitcher\:\:\$viewModes type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Control/ViewModeSwitcher.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Cannot access offset string on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 2 path: library/Icingadb/Web/Controller.php - message: '#^Cannot call method add\(\) on ipl\\Html\\Contract\\Wrappable\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Cannot call method getAdditional\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Cannot call method getPreferences\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Cannot call method getUsername\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 2 path: library/Icingadb/Web/Controller.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Web/Controller.php - message: '#^Cannot call method setAdditional\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 2 path: library/Icingadb/Web/Controller.php - message: '#^Cannot call method setErrorHandlerModule\(\) on array\|Zend_Controller_Plugin_Abstract\|false\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Controller\:\:assertRouteAccess\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Controller\:\:createColumnControl\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Controller\:\:createSearchBar\(\) has parameter \$params with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Controller\:\:export\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Controller\:\:fetchFilterColumns\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Controller\:\:handleSearchRequest\(\) has parameter \$additionalColumns with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Controller\:\:moduleInit\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Controller\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Controller\:\:prepareSearchFilter\(\) has parameter \$additionalColumns with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Controller.php - message: '#^PHPDoc tag @param references unknown parameter\: \$preserveParams$#' identifier: parameter.notFound count: 1 path: library/Icingadb/Web/Controller.php - message: '#^PHPDoc tag @param references unknown parameter\: \$redirectUrl$#' identifier: parameter.notFound count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#1 \$defaultViewMode of method Icinga\\Module\\Icingadb\\Web\\Control\\ViewModeSwitcher\:\:setDefaultViewMode\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Web\\Controller\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#1 \$json of static method Icinga\\Util\\Json\:\:decode\(\) expects string, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Web\\Controller\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#1 \$string of function trim expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#2 \$string of function explode expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#2 \$user of static method Icinga\\User\\Preferences\\PreferencesStore\:\:create\(\) expects Icinga\\User, Icinga\\User\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#2 \$value of method Icinga\\Web\\Url\:\:setParam\(\) expects array\|bool\|string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#3 \$default of method Icinga\\User\\Preferences\:\:getValue\(\) expects null, string given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Parameter \#3 \$filter of method Icinga\\Module\\Icingadb\\Web\\Controller\:\:prepareSearchFilter\(\) expects ipl\\Stdlib\\Filter\\Any, ipl\\Stdlib\\Filter\\Chain given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Property Icinga\\Module\\Icingadb\\Web\\Controller\:\:\$format \(string\|null\) does not accept mixed\.$#' identifier: assign.propertyType count: 1 path: library/Icingadb/Web/Controller.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Navigation/Action.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Navigation/Action.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Web/Navigation/Action.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Web/Navigation/Action.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Action\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Web/Navigation/Action.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Navigation/Action.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Web\\Navigation\\Action\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Web/Navigation/Action.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Web\\Navigation\\Action\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Web/Navigation/Action.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Web/Navigation/Action.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Web/Navigation/Action.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Cannot access property \$hosts_down_unhandled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Cannot cast mixed to int\.$#' identifier: cast.int count: 1 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\HostProblemsBadge\:\:fetchProblemsCount\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\HostProblemsBadge\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\HostProblemsBadge\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\HostProblemsBadge\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Web/Navigation/Renderer/HostProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ProblemsBadge\:\:createBadge\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Navigation/Renderer/ProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ProblemsBadge\:\:disableLink\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Navigation/Renderer/ProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ProblemsBadge\:\:fetchProblemsCount\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Navigation/Renderer/ProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ProblemsBadge\:\:getProblemsCount\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Navigation/Renderer/ProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ProblemsBadge\:\:getUrl\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Navigation/Renderer/ProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ProblemsBadge\:\:round\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Navigation/Renderer/ProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ProblemsBadge\:\:round\(\) has parameter \$count with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Web/Navigation/Renderer/ProblemsBadge.php - message: '#^Property Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ProblemsBadge\:\:\$linkDisabled has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Web/Navigation/Renderer/ProblemsBadge.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Cannot access property \$services_critical_unhandled on ipl\\Orm\\Model\|null\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Cannot cast mixed to int\.$#' identifier: cast.int count: 1 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ServiceProblemsBadge\:\:fetchProblemsCount\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ServiceProblemsBadge\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ServiceProblemsBadge\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\ServiceProblemsBadge\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php - message: '#^Cannot call method getRenderer\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Web/Navigation/Renderer/TotalProblemsBadge.php - message: '#^Property Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\TotalProblemsBadge\:\:\$severityStateMap type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Navigation/Renderer/TotalProblemsBadge.php - message: '#^Property Icinga\\Module\\Icingadb\\Web\\Navigation\\Renderer\\TotalProblemsBadge\:\:\$stateSeverityMap type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Web/Navigation/Renderer/TotalProblemsBadge.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\CheckAttempt\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/CheckAttempt.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CheckStatistics\:\:__construct\(\) has parameter \$object with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Detail/CheckStatistics.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CheckStatistics\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/CheckStatistics.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CheckStatistics\:\:assembleBody\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/CheckStatistics.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CheckStatistics\:\:assembleHeader\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/CheckStatistics.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\CheckStatistics\:\:\$object has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/CheckStatistics.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CommentDetail\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CommentDetail\:\:createComment\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CommentDetail\:\:createDetails\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CommentDetail\:\:createRemoveCommentForm\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CommentDetail\:\:createTicketLinks\(\) has parameter \$text with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CommentDetail\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Widget\\Detail\\CommentDetail\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Widget\\Detail\\CommentDetail\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\CommentDetail\:\:\$comment has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/CommentDetail.php - message: '#^Call to an undefined method ipl\\Html\\Contract\\Wrappable\:\:addHtml\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/CustomVarTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CustomVarTable\:\:__construct\(\) has parameter \$data with no value type specified in iterable type iterable\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/CustomVarTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CustomVarTable\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/CustomVarTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CustomVarTable\:\:renderArray\(\) has parameter \$array with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/CustomVarTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CustomVarTable\:\:renderGroup\(\) has parameter \$entries with no value type specified in iterable type iterable\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/CustomVarTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\CustomVarTable\:\:renderObject\(\) has parameter \$object with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/CustomVarTable.php - message: '#^Parameter \#1 \$name of method Icinga\\Module\\Icingadb\\Widget\\Detail\\CustomVarTable\:\:renderArray\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/CustomVarTable.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\CustomVarTable\:\:\$data \(array\) does not accept iterable\.$#' identifier: assign.propertyType count: 1 path: library/Icingadb/Widget/Detail/CustomVarTable.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\CustomVarTable\:\:\$data type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/CustomVarTable.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\CustomVarTable\:\:\$groups type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/CustomVarTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeCard\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/DowntimeCard.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeCard\:\:calcRelativeLeft\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/DowntimeCard.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeCard\:\:calcRelativeLeft\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Detail/DowntimeCard.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeCard\:\:\$downtime has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/DowntimeCard.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeCard\:\:\$duration has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/DowntimeCard.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeCard\:\:\$end has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/DowntimeCard.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeCard\:\:\$start has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/DowntimeCard.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Cannot access property \$host on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Cannot access property \$is_flexible on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 3 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Cannot access property \$object_type on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Cannot access property \$service on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Cannot access property \$state on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Cannot call method getStateText\(\) on mixed\.$#' identifier: method.nonObject count: 3 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeDetail\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeDetail\:\:createCancelDowntimeForm\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeDetail\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeDetail\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Parameter \#1 \$downtime of static method Icinga\\Module\\Icingadb\\Common\\Links\:\:downtime\(\) expects Icinga\\Module\\Icingadb\\Model\\Downtime, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Widget\\Detail\\DowntimeDetail\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/DowntimeDetail.php - message: '#^Call to an undefined method ipl\\Html\\BaseHtmlElement\|ipl\\Html\\FormattedString\:\:addAttributes\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot access property \$checkcommand_name on mixed\.$#' identifier: property.nonObject count: 6 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot access property \$display_name on mixed\.$#' identifier: property.nonObject count: 18 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot access property \$downtime_id on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot access property \$host on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot access property \$is_flexible on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot access property \$object_type on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot access property \$service on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot call method getStateText\(\) on mixed\.$#' identifier: method.nonObject count: 3 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot call method limit\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Cannot cast mixed to int\.$#' identifier: cast.int count: 2 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleAcknowledgeEvent\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleCommentEvent\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleDowntimeEvent\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleFlappingEvent\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleNotificationEvent\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleStateChangeEvent\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:createExtensions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:createTicketLinks\(\) has parameter \$text with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Parameter \#1 \$acknowledgement of method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleAcknowledgeEvent\(\) expects Icinga\\Module\\Icingadb\\Model\\AcknowledgementHistory, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Parameter \#1 \$comment of method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleCommentEvent\(\) expects Icinga\\Module\\Icingadb\\Model\\CommentHistory, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Parameter \#1 \$downtime of method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleDowntimeEvent\(\) expects Icinga\\Module\\Icingadb\\Model\\DowntimeHistory, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Parameter \#1 \$flapping of method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleFlappingEvent\(\) expects Icinga\\Module\\Icingadb\\Model\\FlappingHistory, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Parameter \#1 \$notification of method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleNotificationEvent\(\) expects Icinga\\Module\\Icingadb\\Model\\NotificationHistory, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Parameter \#1 \$stateChange of method Icinga\\Module\\Icingadb\\Widget\\Detail\\EventDetail\:\:assembleStateChangeEvent\(\) expects Icinga\\Module\\Icingadb\\Model\\StateHistory, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/EventDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\HostDetail\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/HostDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\HostDetail\:\:createServiceStatistics\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/HostDetail.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\HostDetail\:\:\$serviceSummary has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/HostDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\HostInspectionDetail\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/HostInspectionDetail.php - message: '#^Cannot access property \$last_state_change on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/Detail/HostMetaInfo.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\HostMetaInfo\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/HostMetaInfo.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\HostStatistics\:\:__construct\(\) has parameter \$summary with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Detail/HostStatistics.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\HostStatistics\:\:\$summary has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/HostStatistics.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\MultiselectQuickActions\:\:__construct\(\) has parameter \$summary with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\MultiselectQuickActions\:\:__construct\(\) has parameter \$type with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\MultiselectQuickActions\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\MultiselectQuickActions\:\:assembleAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\MultiselectQuickActions\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Widget\\Detail\\MultiselectQuickActions\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Widget\\Detail\\MultiselectQuickActions\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Parameter \#3 \$filter of method Icinga\\Module\\Icingadb\\Widget\\Detail\\MultiselectQuickActions\:\:isGrantedOnType\(\) expects ipl\\Stdlib\\Filter\\Rule, ipl\\Stdlib\\Filter\\Rule\|null given\.$#' identifier: argument.type count: 10 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\MultiselectQuickActions\:\:\$summary has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\MultiselectQuickActions\:\:\$type has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php - message: '#^Access to an undefined property object\:\:\$checkcommand_name\.$#' identifier: property.notFound count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Access to an undefined property object\:\:\$normalized_performance_data\.$#' identifier: property.notFound count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Cannot cast mixed to int\.$#' identifier: cast.int count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:__construct\(\) has parameter \$object with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:compatObject\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createActions\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createCheckStatistics\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createComments\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createCustomVars\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createDowntimes\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createExtensions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createFeatureToggles\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createGroups\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createNotes\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createNotifications\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createPerformanceData\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createPluginOutput\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:createPrintHeader\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:fetchCustomVars\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:getUsersAndUsergroups\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Parameter \#1 \$object of static method Icinga\\Module\\Icingadb\\Util\\PluginOutput\:\:fromObject\(\) expects Icinga\\Module\\Icingadb\\Model\\Host\|Icinga\\Module\\Icingadb\\Model\\Service, object given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:\$compatObject has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:\$object has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectDetail\:\:\$objectType has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/ObjectDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectStatistics\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/ObjectStatistics.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectsDetail\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/ObjectsDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectsDetail\:\:createComments\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectsDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectsDetail\:\:createDowntimes\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectsDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectsDetail\:\:createExtensions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectsDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectsDetail\:\:createFeatureToggles\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectsDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectsDetail\:\:createSummary\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/ObjectsDetail.php - message: '#^Parameter \#3 \$baseFilter of static method Icinga\\Module\\Icingadb\\Hook\\ExtensionHook\\ObjectsDetailExtensionHook\:\:loadExtensions\(\) expects ipl\\Stdlib\\Filter\\Rule, ipl\\Stdlib\\Filter\\Rule\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/ObjectsDetail.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectsDetail\:\:\$query has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/ObjectsDetail.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectsDetail\:\:\$summary has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/ObjectsDetail.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\ObjectsDetail\:\:\$type has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/ObjectsDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\PerfDataTable\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/PerfDataTable.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Cannot access property \$is_acknowledged on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Cannot access property \$is_problem on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\QuickActions\:\:__construct\(\) has parameter \$object with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\QuickActions\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\QuickActions\:\:assembleAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\QuickActions\:\:getLink\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\QuickActions\:\:getLink\(\) has parameter \$action with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\QuickActions\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Widget\\Detail\\QuickActions\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Widget\\Detail\\QuickActions\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/QuickActions.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ServiceDetail\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/ServiceDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ServiceInspectionDetail\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/ServiceInspectionDetail.php - message: '#^Cannot access property \$last_state_change on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/Detail/ServiceMetaInfo.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ServiceMetaInfo\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/ServiceMetaInfo.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\ServiceStatistics\:\:__construct\(\) has parameter \$summary with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Detail/ServiceStatistics.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Detail\\ServiceStatistics\:\:\$summary has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Detail/ServiceStatistics.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Cannot access property \$display_name on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Cannot call method getModel\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Cannot call method limit\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:createCustomVars\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:createExtensions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:createUserDetail\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:createUsergroupList\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:localizeStates\(\) has parameter \$states with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:localizeStates\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:localizeTypes\(\) has parameter \$types with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:localizeTypes\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:separateStates\(\) has parameter \$states with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:separateStates\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Parameter \#1 \$query of method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:applyRestrictions\(\) expects ipl\\Orm\\Query, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Widget\\Detail\\UserDetail\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/UserDetail.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:getColumn\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Call to an undefined method ipl\\Stdlib\\Filter\\Rule\:\:metaData\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Cannot call method getModel\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Cannot call method getRoles\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Cannot call method isUnrestricted\(\) on Icinga\\User\|null\.$#' identifier: method.nonObject count: 4 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Cannot call method limit\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UsergroupDetail\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UsergroupDetail\:\:createCustomVars\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UsergroupDetail\:\:createExtensions\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UsergroupDetail\:\:createPrintHeader\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UsergroupDetail\:\:createUserList\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Detail\\UsergroupDetail\:\:parseDenylist\(\) should return ipl\\Stdlib\\Filter\\None but returns ipl\\Stdlib\\Filter\\Chain\.$#' identifier: return.type count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Parameter \#1 \$condition of method ipl\\Sql\\QueryBuilder\:\:buildCondition\(\) expects array, array\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Parameter \#1 \$denylist of method Icinga\\Module\\Icingadb\\Widget\\Detail\\UsergroupDetail\:\:parseDenylist\(\) expects string, array\ given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Parameter \#1 \$query of method Icinga\\Module\\Icingadb\\Widget\\Detail\\UsergroupDetail\:\:applyRestrictions\(\) expects ipl\\Orm\\Query, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Parameter \#1 \$queryString of method Icinga\\Module\\Icingadb\\Widget\\Detail\\UsergroupDetail\:\:parseRestriction\(\) expects string, array\ given\.$#' identifier: argument.type count: 3 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\\|int\<1, max\>\|string given\.$#' identifier: argument.type count: 4 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Parameter \#2 \$tableName of method ipl\\Orm\\Resolver\:\:qualifyColumn\(\) expects string, int\|string given\.$#' identifier: argument.type count: 2 path: library/Icingadb/Widget/Detail/UsergroupDetail.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Health\:\:__construct\(\) has parameter \$data with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/Health.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\Health\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/Health.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\Health\:\:\$data has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/Health.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\HostStateBadges\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/HostStateBadges.php - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/HostStatusBar.php - message: '#^Parameter \#3 \$number of function tp expects int\|null, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/HostStatusBar.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\HostSummaryDonut\:\:assembleBody\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/HostSummaryDonut.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\HostSummaryDonut\:\:assembleFooter\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/HostSummaryDonut.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\HostSummaryDonut\:\:assembleHeader\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/HostSummaryDonut.php - message: '#^Parameter \#1 \$rule of method ipl\\Stdlib\\Filter\\Chain\:\:add\(\) expects ipl\\Stdlib\\Filter\\Rule, ipl\\Stdlib\\Filter\\Rule\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/HostSummaryDonut.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\IconImage\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/IconImage.php - message: '#^Access to an undefined property object\:\:\$name\.$#' identifier: property.notFound count: 7 path: library/Icingadb/Widget/ItemList/CommandTransportListItem.php - message: '#^Call to an undefined method ipl\\Web\\Common\\BaseItemList\:\:addDetailFilterAttribute\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/ItemList/CommandTransportListItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemList\\PageSeparatorItem\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemList/PageSeparatorItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\BaseStateRowItem\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/BaseStateRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\BaseStateRowItem\:\:assembleCell\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/BaseStateRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\BaseStateRowItem\:\:assembleCell\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/ItemTable/BaseStateRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\BaseStateRowItem\:\:assembleVisual\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/BaseStateRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\BaseStateRowItem\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/BaseStateRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\HostItemTable\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/HostItemTable.php - message: '#^Cannot access property \$display_name on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/ItemTable/HostRowItem.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/ItemTable/HostRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\HostRowItem\:\:assembleCell\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/HostRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\HostRowItem\:\:assembleCell\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/ItemTable/HostRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\HostRowItem\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/HostRowItem.php - message: '#^Parameter \#1 \$service of static method Icinga\\Module\\Icingadb\\Common\\Links\:\:service\(\) expects Icinga\\Module\\Icingadb\\Model\\Service, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/ItemTable/HostRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\ServiceItemTable\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/ServiceItemTable.php - message: '#^Cannot access property \$display_name on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/ItemTable/ServiceRowItem.php - message: '#^Cannot access property \$name on mixed\.$#' identifier: property.nonObject count: 3 path: library/Icingadb/Widget/ItemTable/ServiceRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\ServiceRowItem\:\:assembleCell\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/ServiceRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\ServiceRowItem\:\:assembleCell\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/ItemTable/ServiceRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\ServiceRowItem\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/ServiceRowItem.php - message: '#^Parameter \#1 \$host of static method Icinga\\Module\\Icingadb\\Common\\Links\:\:host\(\) expects Icinga\\Module\\Icingadb\\Model\\Host, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/ItemTable/ServiceRowItem.php - message: '#^Parameter \#2 \$host of static method Icinga\\Module\\Icingadb\\Common\\Links\:\:service\(\) expects Icinga\\Module\\Icingadb\\Model\\Host, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/ItemTable/ServiceRowItem.php - message: '#^Call to an undefined method ipl\\Html\\Contract\\FormElement\:\:addHtml\(\)\.$#' identifier: method.notFound count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Cannot access offset 0 on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Cannot access offset 1 on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateItemTable\:\:__construct\(\) has parameter \$data with no value type specified in iterable type iterable\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateItemTable\:\:applyColumnMetaData\(\) has parameter \$columns with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateItemTable\:\:applyColumnMetaData\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateItemTable\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateItemTable\:\:assembleColumnHeader\(\) has parameter \$label with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateItemTable\:\:getVisualLabel\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateItemTable\:\:init\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Parameter \#1 \.\.\.\$content of method ipl\\Html\\BaseHtmlElement\:\:addHtml\(\) expects ipl\\Html\\ValidHtml, object given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateItemTable\:\:\$baseAttributes has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateItemTable\:\:\$data type has no value type specified in iterable type iterable\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateItemTable\:\:\$sort \(string\) does not accept string\|null\.$#' identifier: assign.propertyType count: 1 path: library/Icingadb/Widget/ItemTable/StateItemTable.php - message: '#^Cannot access property \$check_attempt on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$hard_state on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$is_handled on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$is_problem on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$is_reachable on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$last_state_change on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$last_update on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$next_check on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$next_update on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$normalized_performance_data on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$previous_hard_state on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$previous_soft_state on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$soft_state on mixed\.$#' identifier: property.nonObject count: 2 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot access property \$state_type on mixed\.$#' identifier: property.nonObject count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot call method getIcon\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot call method getStateText\(\) on mixed\.$#' identifier: method.nonObject count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Cannot cast mixed to int\.$#' identifier: cast.int count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateRowItem\:\:assembleCell\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateRowItem\:\:assembleCell\(\) has parameter \$value with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ItemTable\\StateRowItem\:\:assembleVisual\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Parameter \#1 \$object of static method Icinga\\Module\\Icingadb\\Util\\PluginOutput\:\:fromObject\(\) expects Icinga\\Module\\Icingadb\\Model\\Host\|Icinga\\Module\\Icingadb\\Model\\Service, ipl\\Orm\\Model given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Parameter \#2 \$alt of class Icinga\\Module\\Icingadb\\Widget\\IconImage constructor expects string\|null, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/ItemTable/StateRowItem.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ServiceStateBadges\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ServiceStateBadges.php - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/ServiceStatusBar.php - message: '#^Parameter \#3 \$number of function tp expects int\|null, mixed given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/ServiceStatusBar.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ServiceSummaryDonut\:\:assembleBody\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ServiceSummaryDonut.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ServiceSummaryDonut\:\:assembleFooter\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ServiceSummaryDonut.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\ServiceSummaryDonut\:\:assembleHeader\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/ServiceSummaryDonut.php - message: '#^Parameter \#1 \$rule of method ipl\\Stdlib\\Filter\\Chain\:\:add\(\) expects ipl\\Stdlib\\Filter\\Rule, ipl\\Stdlib\\Filter\\Rule\|null given\.$#' identifier: argument.type count: 1 path: library/Icingadb/Widget/ServiceSummaryDonut.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\StateChange\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/StateChange.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\StateChange\:\:setHandled\(\) has parameter \$isHandled with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/StateChange.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\StateChange\:\:setIcon\(\) has parameter \$icon with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/StateChange.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\StateChange\:\:\$currentStateBallSize has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/StateChange.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\StateChange\:\:\$previousState has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/StateChange.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\StateChange\:\:\$previousStateBallSize has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/StateChange.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\StateChange\:\:\$state has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/StateChange.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\TagList\:\:addLink\(\) has parameter \$content with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/TagList.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\TagList\:\:addLink\(\) has parameter \$url with no type specified\.$#' identifier: missingType.parameter count: 1 path: library/Icingadb/Widget/TagList.php - message: '#^Method Icinga\\Module\\Icingadb\\Widget\\TagList\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: library/Icingadb/Widget/TagList.php - message: '#^Property Icinga\\Module\\Icingadb\\Widget\\TagList\:\:\$content has no type specified\.$#' identifier: missingType.property count: 1 path: library/Icingadb/Widget/TagList.php icingadb-web-1.4.0/phpstan.neon000066400000000000000000000013161516124654200163740ustar00rootroot00000000000000includes: - phpstan-baseline.neon parameters: level: max checkFunctionNameCase: true checkInternalClassCaseSensitivity: true treatPhpDocTypesAsCertain: false paths: - application - library scanDirectories: - /icingaweb2 - /usr/share/icinga-php - /usr/share/icingaweb2-modules ignoreErrors: - messages: - '#Unsafe usage of new static\(\)#' - '#. but return statement is missing#' reportUnmatched: false universalObjectCratesClasses: - ipl\Orm\Model - Icinga\Web\View - Icinga\Data\ConfigObject - Icinga\Module\Monitoring\Object\MonitoredObject icingadb-web-1.4.0/public/000077500000000000000000000000001516124654200153135ustar00rootroot00000000000000icingadb-web-1.4.0/public/css/000077500000000000000000000000001516124654200161035ustar00rootroot00000000000000icingadb-web-1.4.0/public/css/common.less000066400000000000000000000116621516124654200202710ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2019 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later @exports: { @iplWebAssets: "../lib/icinga/icinga-php-library"; }; & > .content.full-height { padding-top: 0; padding-bottom: 0; } .plugin-output { .monospace(); word-break: break-word; } div.show-more { .clearfix(); float: right; } .state-ball.state-not-available { .ball-solid(@gray-light); .animate(pulse 1.5s infinite both); } .state-ball.state-info { .ball-solid(@default-text-color); } .icon-ball { .ball(); .ball-outline(@gray); text-align: center; i:before { margin-right: 0; } } .user-ball { .ball-solid(@gray-semilight); font-weight: bold; text-transform: uppercase; vertical-align: top; // Avoid the bottom margin of the ball to affect the line height } .usergroup-ball { .ball-outline(@gray); text-transform: uppercase; vertical-align: top; // Avoid the bottom margin of the ball to affect the line height } .ack-badge { text-transform: uppercase; line-height: 1; margin-top: -.25em; align-self: flex-start; i { vertical-align: baseline; } } .controls { .box-shadow(0, 0, 0, 1px, @gray-lighter); flex-shrink: 0; position: relative; // Required for the host meta info control z-index: 1; // The content may clip, this ensures the separator is always visible > :not(:only-child) { margin-bottom: .5em; } #object-meta-info { margin-bottom: 0; .object-meta-info { margin-bottom: .5em; } } &.overdue, &.overdue .tabs li.active a, &.overdue .object-meta-info-control { background-color: @gray-lighter; } .limit-control, .view-mode-switcher, .sort-control { margin-left: .5em; float: right; } .toggle-switch { margin-top: 0; margin-bottom: 0; } .search-controls .continue-with { margin-right: -.5em; margin-left: .5em; } .show-more { margin-top: .25em; } .notice { display: none; } // TODO: Remove once ipl-web v0.7.0 is required &:not(.default-layout) { .pagination-control { float: left; } .sort-control { display: flex; justify-content: flex-end; :not(.form-element) > label { margin-right: 0; } .control-button { margin: 0; } } > :not(:only-child) { margin-bottom: 0.5em; } .search-suggestions { margin-bottom: 2.5em; } .search-controls { clear: both; display: flex; min-width: 100%; .search-bar { flex: 1 1 auto; & ~ .control-button:last-child { margin-right: -.5em; } & ~ .control-button { margin-left: .5em; } } } } } .content > h2:first-child, .object-detail > h2:first-child { margin-top: 0; } .content.full-width > h2 { margin-left: 1em / 1.333em; // 1em / h2 font size } .object-detail :not(.caption) > .plugin-output { .rounded-corners(.25em); background-color: @gray-lighter; padding: .5em; } .object-detail .item-list { &.action-list .list-item { margin-right: -1em; margin-left: -1em; padding-right: 1em; padding-left: 1em; } .list-item:last-of-type .caption { min-height: 1.5em; max-height: 3em; height: auto; } } .perfdata-wrapper { svg { width: 100%; } svg:not(:last-child) { margin-bottom: 1em; } } .text-center { text-align: center; } .text-muted { color: @gray; } .accompanying-text { color: @text-color-light; .subject { color: @text-color; .user-select(all); } } .comment-detail { > form, > h2:not(:first-child) { margin-top: 1em; } } .downtime-detail { .downtime-progress { margin-bottom: 1em; } } .content + .footer { display: flex; .box-shadow(0, -1px, 0, 0, @gray-lighter); color: @text-color-light; .selection-count { flex: 1 1 auto; .selected-items { font-size: 1.25em; &.hint { opacity: 0.75; } } } .status-bar, .selection-count { font-size: .857em; padding: .25em 1em; line-height: 1.7; } } .status-bar { margin-left: auto; .item-count { font-size: 1.25em; } .state-badges { display: inline-block; margin: 0 0 0 .417em; } } .multiselect-summary { display: flex; align-items: center; justify-content: flex-start; // Donut > div:first-child { height: 4em; width: 4em; > svg { height: auto; max-width: 100%; } } > .vertical-key-value { padding: 0 .5em; } } .header-item-layout.host, .header-item-layout.service, .item-layout.history { .visual.small-state-change .state-change { padding-top: .25em; } } // user, usergroup and comment balls .header-item-layout .visual:has(.ball.ball-size-xl) { padding-top: 0; padding-bottom: 0; } form[name="form_confirm_removal"] { text-align: center; } .affected-objects { display: inline-flex; align-items: baseline; color: @state-critical; .icon { opacity: .9; } } icingadb-web-1.4.0/public/css/form/000077500000000000000000000000001516124654200170465ustar00rootroot00000000000000icingadb-web-1.4.0/public/css/form/schedule-service-downtime-form.less000066400000000000000000000006341516124654200257600ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .downtime-duration { > label { display: flex; flex: 1 1 auto; flex-flow: row-reverse; input { flex: 1 1 auto; } span { margin-left: .5em; padding: .5625em 0; line-height: 1.1em; } &:not(:last-child) { margin-right: 1.5em; } } } icingadb-web-1.4.0/public/css/item/000077500000000000000000000000001516124654200170415ustar00rootroot00000000000000icingadb-web-1.4.0/public/css/item/downtime.less000066400000000000000000000027071516124654200215650ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later // Style .item-layout.downtime { .visual { background-color: @gray-lighter; } &.in-effect .visual { background-color: @color-ok; color: @text-color-on-icinga-blue; } } .list-item.downtime { .progress { > .bar { background-color: @color-ok; } } .main { border-top: 1px solid @gray-light; } &:first-child .main { border-top: none; .progress > .bar { border-top: 1px solid @gray-light; } } } // Layout .item-layout.downtime { .caption > * { display: inline; } .visual { justify-content: center; flex-shrink: 0; line-height: 1em; padding: .5em .25em; text-align: center; width: 6em; strong { font-size: 1.5em; line-height: 1em; } } } .list-item.downtime { .progress { height: 2px; margin-bottom: ~"calc(.5em - 2px)"; min-width: 100%; position: relative; > .bar { height: 100%; max-width: 100%; } } &:first-child .main .progress > .bar { height: ~"calc(100% + 1px)"; // +1px due to the border added exclusively for the first item } .main:has(.progress) { padding-top: 0; } } .minimal-item-layout.downtime { .visual { display: block; line-height: 1.5; width: 8em; white-space: nowrap; strong { display: inline-block; font-size: 1em; } } } icingadb-web-1.4.0/public/css/item/hostgroup.less000066400000000000000000000010251516124654200217610ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2025 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .hostgroup { &.table-row.item-layout { > .col:has(.object-statistics) { align-content: center; // Actually, not sure why this works… .col is not a grid container } > .main > .caption { height: auto; .text-ellipsis(); .line-clamp("reset"); } } &.header-item-layout > .main { padding-top: 0; padding-bottom: 0; > header { align-items: center; } } } icingadb-web-1.4.0/public/css/item/icon-image.less000066400000000000000000000012161516124654200217410ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2025 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .item-layout { .icon-image { width: 3em; height: 3em; text-align: center; margin-top: .5em; margin-right: .5em; overflow: hidden; img { max-height: 100%; max-width: 100%; height: auto; width: auto; } .icon, img { vertical-align: baseline; } } &.minimal-item-layout { .icon-image { height: 2em; width: 2em; line-height: 2; } } &.header-item-layout .icon-image { margin-top: 0; img { vertical-align: middle; } } } icingadb-web-1.4.0/public/css/item/item-layout.less000066400000000000000000000026761516124654200222150ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later // Style .item-layout { &.overdue { background-color: @gray-lighter; } &.overdue header > *:not(time), &.overdue .caption { opacity: 0.6; } &.overdue time { .rounded-corners(); background-color: @color-critical; color: @text-color-on-icinga-blue; } .title { .state-text { color: @text-color; text-transform: uppercase; } } footer { .status-icons { color: @text-color-light; .icon { opacity: .5; } } } } // Layout .item-layout { &.overdue time { margin-right: -.5em; padding: 0 0.5em; } .visual .check-attempt { margin-top: .5em; } .caption { &.plugin-output, .plugin-output { font-size: 11/12em; line-height: 1.5*12/11em; } } &.minimal-item-layout, &.default-item-layout { .caption .plugin-output { .text-ellipsis(); } } &.minimal-item-layout.hostgroup .extended-info { > :not(:last-child) { margin-right: 1em; } } footer { .status-icons { display: flex; align-items: center; } .performance-data { margin-left: auto; .inline-pie { display: inline-block; line-height: 1.5*.857em; height: 1em; width: 1em; &:not(:last-child) { margin-right: .209em; } } } } } icingadb-web-1.4.0/public/css/item/redundancy-group.less000066400000000000000000000005611516124654200232210ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2024 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .item-layout.redundancy-group { .caption { display: flex; justify-content: flex-end; } } .default-item-layout.redundancy-group { .title > .subject { margin-right: .28125em; } .caption .object-statistics { font-size: 0.75em; } } icingadb-web-1.4.0/public/css/item/servicegroup.less000066400000000000000000000002171516124654200224460ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2025 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .servicegroup { .hostgroup; } icingadb-web-1.4.0/public/css/list/000077500000000000000000000000001516124654200170565ustar00rootroot00000000000000icingadb-web-1.4.0/public/css/list/action-list.less000066400000000000000000000006611516124654200221770ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .action-list { [data-action-item]:hover { background-color: @tr-hover-color; cursor: pointer; } [data-action-item].active { background-color: @tr-active-color; } &[data-icinga-multiselect-url] * { user-select: none; } } #layout.minimal-layout .footer > .selection-count { display: none; } icingadb-web-1.4.0/public/css/list/item-list.less000066400000000000000000000024351516124654200216610ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later // Style .item-list { .load-more:hover, .page-separator:hover { background: none; } > .load-more a { .rounded-corners(.25em); background: @low-sat-blue; text-align: center; &:hover { opacity: .8; text-decoration: none; } } > .page-separator:after { content: ""; display: block; width: 100%; height: 1px; background: @gray; align-self: center; margin-left: .25em; } > .page-separator a { color: @gray; font-weight: bold; &:hover { text-decoration: none; } } > .page-separator + .list-item .main { border-top: none; } } // Layout .item-list .load-more { display: flex; a { flex: 1; margin: 1.5em 0; padding: .5em 0; } } .item-list { .detailed-item-layout footer { .status-icons .icon, .performance-data { font-size: .857em; line-height: 1.5*.857em; } } // Not sure what this is for. Maybe user content? (Markdown) But why in the title?? .default-item-layout .title { p { margin: 0; } } .minimal-item-layout .title { p { display: inline; & + p { margin-left: .417em; } } } } icingadb-web-1.4.0/public/css/list/state-item-table.less000066400000000000000000000065071516124654200231170ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2022 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later // Style .state-item-table { padding: 0; thead { th { font-weight: normal; // Border styles start form { padding: 0 0 0 1px; border-bottom: 1px solid @gray-light; background: linear-gradient(to top, @gray-light, @body-bg-color); button { background: @body-bg-color; } } &:first-child form { padding-left: 0; } // Border styles end } button { .appearance(none); border: none; background: none; padding: .1em .5em; text-align: left; color: @text-color-light; > .icon { opacity: 0; width: 0; transition: opacity .25s linear, width .25s ease; } &:hover .icon, &:focus .icon, &.active .icon { opacity: 1; width: 1em; } &.active { font-weight: bold; } } } .list-item:not(:last-child) > *:not(.visual), .row-item:not(:last-child) { border-bottom: 1px solid @gray-light; } } @media print { .list-item.page-break-follows { &:not(:last-child) > *:not(.visual) { border-bottom: none; } } } // Layout table.state-item-table { table-layout: fixed; } .state-item-table { display: table; width: 100%; margin: 0; thead { position: sticky; top: 0; th { // That's layout, yes, controls overflow when scrolling padding: 1em 0 0 0; background: @body-bg-color; } th button { width: 100%; display: inline-flex; align-items: baseline; justify-content: space-between; span { .text-ellipsis(); } } } th.has-visual { width: 3em; } tbody td { .text-ellipsis(); vertical-align: top; } .list-item { display: table-row; } .list-item > .col { display: table-cell; vertical-align: middle; white-space: nowrap; &:not(:last-child) { padding-right: 1em; } &.title { .text-ellipsis(); width: 100%; } > * { display: inline-block; &:not(:last-child) { margin-right: .5em; } } } .list-item > *:not(.visual) { padding: .5em 0; } .list-item > .visual { display: table-cell; padding: .5em 1em 0 0; } > .empty-state-bar, > tbody > tr:first-child .empty-state-bar { margin: 0 1em; } } .content.full-width .state-item-table .list-item { // The .list-item itself can't have padding because of `display:table-row` &:before, &:after { display: inline-block; content: '\00a0'; width: 1em; } } #layout.twocols table.state-item-table { > thead > tr > th, > tbody > tr > td { &:nth-child(n+6) { display: none; width: 0; } } } #layout.wide-layout .item-table th.has-plugin-output { width: 50em; } #layout.default-layout .item-table th.has-plugin-output { width: 30em; } #layout.compact-layout .item-table th.has-plugin-output { width: 10em; } #layout.twocols table.item-table { .has-plugin-output { width: auto; } } table.item-table { th.has-visual { button { display: inline-flex; justify-content: center; } span > .icon:before { margin: 0; } } .visual { text-align: center; } } icingadb-web-1.4.0/public/css/list/state-row-item.less000066400000000000000000000012451516124654200226310ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2022 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later // Style .row-item { .plugin-output { overflow: hidden; .line-clamp(); } .subject { font-weight: bold; } } // Layout .row-item { .performance-data { overflow: hidden; .line-clamp(2); white-space: normal; margin-left: -.25em; .inline-pie { display: inline-block; width: 1em; height: 1em; margin-left: .25em; } } .has-icon-images { height: 2.5em; vertical-align: middle; img { max-height: 100%; max-width: 100%; height: auto; width: auto; } } } icingadb-web-1.4.0/public/css/markdown.less000066400000000000000000000021561516124654200206210ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .markdown { > p, > hr, > ul, > ol, > table, > pre, > blockquote, li > ul, li > ol { margin: 1em 0 1em 0; &:first-child { margin-top: 0; } } p { &:last-child { margin-bottom: 0; } } img { max-width: 100%; height: auto; } a { border-bottom: 1px dotted @text-color-light; &:hover, &:focus { border-bottom: 1px solid @text-color; text-decoration: none; } img { max-width: 32em; } &.with-thumbnail { img { padding: 1px; } &:hover, &:focus { img { padding: 0; } } } } table { border-collapse: collapse; th { text-align: left; background-color: @gray-lighter; } &, th, td { border: 1px solid; border-color: @gray-light; } } } .markdown.inline { img { max-height: 100%; vertical-align: middle; } a.with-thumbnail { &, &:hover, &:focus { border-bottom: none; } } } icingadb-web-1.4.0/public/css/mixin/000077500000000000000000000000001516124654200172275ustar00rootroot00000000000000icingadb-web-1.4.0/public/css/mixin/progress-bar.less000066400000000000000000000115271516124654200225330ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .progress-bar() { &.progress-bar { --hPadding: 10%; --duration-scale: 80%; .above, .below { list-style-type: none; margin: 0; padding: 0; position: relative; height: ~"calc(2em + 2px)"; } .below { > .left { // positions with left property position: absolute; left: var(--hPadding); top: 0; } > .right { // positions with right property position: absolute; right: ~"calc(100% - calc(var(--hPadding) + var(--duration-scale)))"; top: 0; } } .positioned { position: absolute; } .bubble { .rounded-corners(.25em); background-color: @body-bg-color; border: 1px solid; border-color: @gray-light; position: relative; box-shadow: 0 0 1em 0 rgba(0, 0, 0, .1); padding: .25em .5em; text-align: center; width: auto; // The wrapper of .bubble is dynamically moved to the left based on the value of the progress bar // This moves the center of the bubble to the beginning of the wrapper regardless of the size of the content. transform: translate(50%, 0); z-index: 1; > * { position: relative; z-index: 2; } &:hover { z-index: 5; } &::before { background-color: @body-bg-color; border-bottom: 1px solid @gray-light; border-right: 1px solid @gray-light; content: ""; display: block; height: 1em; margin-left: -.5em; transform: rotate(45deg); width: 1em; z-index: 1; position: absolute; bottom: -.5em; left: 50%; } &.upwards::before { bottom: auto; top: -7/12em; transform: rotate(225deg); } &.right-aligned { // entire width (moves the left border in place of the right) - (.675em (:before placement) + .5em (half :before width)) - 1px (:before border) transform: translate(~"calc(100% - 1.175em - 1px)", 0); &::before { top: auto; left: 1.175em; bottom: -.5em; } } &.left-aligned { // This is (.675em (:before placement) + .5em (half :before width)) + 1px (:before border) transform: translate(~"calc(1.175em + 1px)", 0); &::before { top: auto; left: auto; right: .675em; bottom: -.5em; } } } .left .bubble { transform: translate(-50%, 0); } .above .positioned { bottom: 0; } .below .positioned { top: 0; } .vertical-key-value { .key { white-space: nowrap; } .value { white-space: nowrap; font-size: 1em; line-height: 1; } } .timeline { @marker-gap: 1/12em; .rounded-corners(.5em); background-color: @gray-lighter; height: 1em; margin: 1em 0; position: relative; width: 100%; z-index: 1; .marker { .rounded-corners(50%); background-color: @gray-light; height: .857em; margin-left: -.857/2em; width: .857em; z-index: 2; position: absolute; top: @marker-gap; &.highlighted { background-color: @icinga-blue; } &.left { left: var(--hPadding); } &.right { left: ~"calc(var(--hPadding) + var(--duration-scale))"; } } .progress { position: absolute; left: var(--hPadding); width: var(--duration-scale); &[data-animate-progress]::before { content: ""; display: block; width: .5em + @marker-gap; height: 1em + (@marker-gap * 2); margin-top: -@marker-gap; .rounded-corners(.5em); border-top-right-radius: 0; border-bottom-right-radius: 0; position: absolute; left: -.5em - @marker-gap; } > .bar { width: 0; // set by progress-bar.js height: 1em + (@marker-gap * 2); margin-top: -@marker-gap; } &::before, > .bar { background-color: @gray-light; } } .timeline-overlay { position: absolute; left: ~"calc(var(--hPadding) + var(--duration-scale))"; width: var(--overlay-scale); height: 1em + (@marker-gap * 2); margin-top: -@marker-gap; opacity: .6; } .progress > .bar, .timeline-overlay { display: flex; justify-content: flex-end; .now { width: .25em; border: solid @default-bg; border-width: 1px 0 1px 0; background-color: red; } } } } } icingadb-web-1.4.0/public/css/mixin/state-badges.less000066400000000000000000000012021516124654200224550ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .state-badges() { &.state-badges { padding: 0; ul { padding: 0; } li { display: inline-block; } li > ul > li:first-child:not(:last-child) .state-badge { border-bottom-right-radius: 0; border-top-right-radius: 0; } li > ul > li:last-child:not(:first-child) .state-badge { border-bottom-left-radius: 0; border-top-left-radius: 0; } > li:not(:last-child) { margin-right: .25em; } li > ul > li + li { margin-left: 1px; } } } icingadb-web-1.4.0/public/css/mixins.less000066400000000000000000000003161516124654200203020ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2019 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .monospace() { font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; } icingadb-web-1.4.0/public/css/view/000077500000000000000000000000001516124654200170555ustar00rootroot00000000000000icingadb-web-1.4.0/public/css/view/service-grid.less000066400000000000000000000016521516124654200223340ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .service-grid-table { width: 0; white-space: nowrap; td { color: @gray-light; padding: 0.2em; text-align: center; width: 1em; } .rotate-45 { height: 10em; div { .transform(translate(0.4em, 2.8em) rotate(315deg)); width: 1.5em; } } .service-grid-table-more { text-align: center; a { display: inline; } } } .joystick-pagination { margin: 0 auto; font-size: 130%; a { color: @text-color; outline: none; &:hover { color: @text-color-light; } &:focus, &:active { color: @icinga-blue; } } i { display: block; height: 1.5em; width: 1.5em; } } .service-grid-link { .bg-stateful(); .rounded-corners(); display: inline-block; height: 1.5em; vertical-align: middle; width: 1.5em; } icingadb-web-1.4.0/public/css/widget/000077500000000000000000000000001516124654200173665ustar00rootroot00000000000000icingadb-web-1.4.0/public/css/widget/actions.less000066400000000000000000000007201516124654200217150ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .object-detail-actions a { border-bottom: 1px solid @gray-light; display: inline-block; margin-bottom: .25em; .text-ellipsis(); max-width: 32em; &:hover { border-color: @icinga-blue-light; color: @icinga-blue; text-decoration: none; } } ul.object-detail-actions { list-style-type: none; padding: 0; margin: 0; } icingadb-web-1.4.0/public/css/widget/check-attempt.less000066400000000000000000000005631516124654200230130ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .check-attempt { display: flex; align-items: center; justify-content: center; } .check-attempt .ball { background-color: @gray-light; &:not(:last-child) { margin-right: 1/6em; } &.ball-size-s { background-color: @gray-semilight; } } icingadb-web-1.4.0/public/css/widget/check-statistics.less000066400000000000000000000067061516124654200235340ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .check-statistics { position: relative; .card(); .progress-bar(); .check-attempt { display: inline-flex; } &.progress-bar .bubble { &.top-left-aligned, &.top-right-aligned { &::before { visibility: hidden; } svg { position: absolute; top: -1em; width: 1em; height: 1em; .bg { fill: @body-bg-color; } .border { fill: @gray-light; } } } &.top-left-aligned { transform: unset; border-top-left-radius: 0; svg { left: -1px; } } &.top-right-aligned { transform: translate(-100%); border-top-right-radius: 0; svg { right: -1px; } } } // ATTENTION!: `&.progress-bar {` must not be used here, seems to confuse the less parser!!!!111 &.progress-bar .timeline .progress.running { &::before, > .bar { background: @state-ok; } } &.progress-bar .check-timeline { margin-top: .5em; } &.progress-bar .above { margin-top: .5em; } .interval-line { position: absolute; height: 100%; &::before { position: absolute; top: ~"calc(50% - .125em)"; display: block; height: .25em; width: 100%; content: ""; background-color: @gray-light; } .vertical-key-value { position: absolute; left: 50%; transform: translate(-50%, 0); padding: 0 .2em; background-color: @body-bg-color; } .start, .end { position: absolute; top: 50%; width: .25em; height: 1em; background-color: @gray; } .start { left: 0; transform: translate(-50%, -50%); } .end { right: 0; transform: translate(50%, -50%); } } .execution-line .vertical-key-value { z-index: 1; } &.check-overdue { --duration-scale: 60%; --overlay-scale: 20%; .above { .now { position: absolute; right: var(--hPadding); bottom: 0; .bubble { // to move the center of the bubble to the end of the wrapper. transform: translate(50%, 0); } } } .timeline-overlay { background: linear-gradient(90deg, @gray-light 0, @color-down 2em); opacity: 1; &::after { background-color: @color-down; } } } &.checks-disabled.progress-bar { .timeline { .marker { &.highlighted { background-color: @gray; } } } } .checks-disabled-overlay { border-radius: 0.4em; position: absolute; top: 0; right: 0; left: 0; bottom: 0; display: flex; flex-wrap: wrap; align-items: center; justify-content: center; background-color: ~"@{disabled-gray}20"; z-index: 1; .notes { color: @text-color-light; margin-top: -4em; text-shadow: 0 0 1px rgba(0, 0, 0, 0.25); } } } #layout.twocols &#col1, #layout.minimal-layout, #layout.poor-layout, #layout.twocols.compact-layout, #layout.twocols.default-layout { .check-statistics .bubble.top-right-aligned { transform: translate(-50%, 0); // default what progress-bar() defined border-top-right-radius: .25em; // default what progress-bar() defined &::before { visibility: visible; } svg { display: none; } } } icingadb-web-1.4.0/public/css/widget/comment-popup.less000066400000000000000000000027231516124654200230650ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .comment-popup { font-size: 1em / .857em; // // default font size / footer font size = 12px height: 6em; border-radius: 0.25em; border: 1px solid; border-color: @gray-light; background-color: @body-bg-color; } .comment-wrapper { position: relative; .comment-popup { position: absolute; top: 2.5em; left: -1.6em; z-index: 1; width: 50em; padding: 0 1em; } .comment-popup:before { content: ''; position: absolute; display: inline-block; width: 1.5em; height: 1.5em; left: 1.5em; top: ~"calc(-0.75em - 1px)"; border-left: 1px solid @gray-light; border-top: 1px solid @gray-light; background-color: @body-bg-color; z-index: -1; transform: rotate(45deg); } } ul.item-list li:nth-last-child(2):not(:first-child), ul.item-list li:last-child:not(:first-child) { .comment-wrapper { .comment-popup { top: -7em; } .comment-popup:before { bottom: ~"calc(-0.75em - 1px)"; top: unset; transform: rotate(225deg); } } } .comment-wrapper:not(:hover) .comment-popup { display: none; } #layout { &.twocols { .comment-popup { width: 35em; } &.poor-layout, &.compact-layout { .comment-popup { width: 25em; } } } &.poor-layout, &.minimal-layout { .comment-popup { width: 25em; } } } icingadb-web-1.4.0/public/css/widget/custom-var-table.less000066400000000000000000000015741516124654200234520ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .custom-var-table { .level-1 th { padding-left: .5em; } .level-2 th { padding-left: 1em; } .level-3 th { padding-left: 1.5em; } .level-4 th { padding-left: 2em; } .level-5 th { padding-left: 2.5em; } .level-6 th { padding-left: 3em; } thead th { padding-left: 0; text-align: left; font-weight: bold; font-size: 1.167em; > span { :nth-child(1), :nth-child(2) { display: none; } } } &[data-can-collapse] thead th > span { :nth-child(1) { display: none; } :nth-child(2) { display: inline-block; } } &.collapsed thead th > span { :nth-child(1) { display: inline-block; } :nth-child(2) { display: none; } } } icingadb-web-1.4.0/public/css/widget/dependency-node-state-badges.less000066400000000000000000000004001516124654200256520ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2024 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .dependency-node-state-badges { .state-badges(); .state-badge { &.state-problem { background-color: @color-critical; } } } icingadb-web-1.4.0/public/css/widget/donut-container.less000066400000000000000000000006061516124654200233710ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .donut-container { .card(); h2 { margin: 0; } .state-badges { text-align: center; } &:not(:last-of-type) { margin-right: 1em; margin-bottom: 1em; } .donut { margin: 0 auto; } } #layout.minimal-layout .donut-container { width: 100%; } icingadb-web-1.4.0/public/css/widget/downtime-card.less000066400000000000000000000004221516124654200230110ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .downtime-progress { .progress-bar(); &.progress-bar .timeline .downtime-elapsed { &::before, > .bar { background-color: @state-ok; } } } icingadb-web-1.4.0/public/css/widget/host-state-badges.less000066400000000000000000000002311516124654200235700ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .host-state-badges { .state-badges(); } icingadb-web-1.4.0/public/css/widget/key-value-list.less000066400000000000000000000005221516124654200231300ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .key-value-list { list-style-type: none; margin: 0; padding: 0; li { display: flex; } li > span { padding: .25em; &.label { display: block; padding-left: 0; width: 12em; } } } icingadb-web-1.4.0/public/css/widget/monitoring-health.less000066400000000000000000000041451516124654200237120ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .monitoring-health { max-width: 65em; > section:not(:last-child) { margin-bottom: 4em; } .vertical-key-value .value { display: inline-block; margin-bottom: .25em; } .check-summary { padding: .5em 0; .col { padding: 0 1em .5em 1em; } .col:not(:last-child) { border-right: 1px solid @gray-light; } } .check-summary, .instance-features { .rounded-corners(); border: 1px solid; border-color: @gray-light; display: flex; width: 100%; } .check-summary, .col-content, .icinga-info { width: 100%; display: flex; flex-wrap: wrap; justify-content: space-around; } .icinga-info { gap: 2em 1em; } .col { flex: 1 1 auto; text-align: center; > h3 { margin: 0 0 1em; } } .icinga-health { .rounded-corners(); margin-bottom: 2em; padding: 1em; text-align: center; width: 100%; font-weight: bold; &.up { border: 1px solid; border-color: @color-up; color: @color-up; } &.down { background-color: @color-down; color: @body-bg-color; } } .instance-features { .control-group { flex: 1 1 auto; margin: .5em 0; padding: .5em; width: 0; display: flex; align-items: center; flex-direction: column-reverse; justify-content: flex-end; &:not(:last-of-type) { border-right: 1px solid @gray-light; } .control-label-group { font-size: 10/12em; margin-right: 0; margin-top: 1em; padding: 0; text-align: center; width: auto; label { text-align: center; } } .toggle-switch { margin: 0; } } } } #layout.minimal-layout { .icinga-info { .vertical-key-value { width: 100%; } } .instance-features { flex-wrap: wrap; .control-group { width: 33%; &:nth-child(3n) { border-right: none; } } } } icingadb-web-1.4.0/public/css/widget/notice.less000066400000000000000000000007711516124654200215440ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2022 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later // Style .notice { @margin: 1em / 1.25; @padding: .75em / 1.25; .rounded-corners(); padding: @padding; color: @text-color-on-icinga-blue; background-color: @state-warning; font-weight: bold; font-size: 1.25em; // Layout display: flex; align-items: baseline; justify-content: space-between; margin: 0 @margin @margin @margin; > span { .text-ellipsis(); } } icingadb-web-1.4.0/public/css/widget/object-features.less000066400000000000000000000016511516124654200233430ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later form.object-features { span.description { text-align: left; } .control-label-group { text-align: left; margin-right: 0; width: @name-value-table-name-width; color: @text-color-light; label { font-size: inherit; } } .control-group { margin-top: 0; margin-bottom: 0; &.indeterminate { justify-content: flex-start; .control-label-group { flex: 0 1 auto; } select { width: auto; flex: 0 1 auto; & + span.hint { flex: 0 1 auto; } } } } .toggle-switch { margin-left: @table-column-padding; } select { margin-right: .5em; margin-left: @table-column-padding; & + span.hint { margin: .35em; color: @gray-light; font-style: italic; } } } icingadb-web-1.4.0/public/css/widget/object-grid.less000066400000000000000000000013011516124654200224420ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later // HostGroup- and -ServiceGroupGrid styles ul.object-grid { display: grid; grid-template-columns: repeat(auto-fit, 15em); grid-gap: 1em 2em; margin: 0; padding: 0; li.item-layout.object-grid-cell { padding: .25em; .rounded-corners(); .caption { height: 1.5em; // Single line .text-ellipsis(); .line-clamp("reset"); } .title, .caption { a { display: inline-block; max-width: 10em; text-align: center; } } .state-badge { width: 2.5em; height: 2.5em; line-height: 2; } } } icingadb-web-1.4.0/public/css/widget/object-inspection.less000066400000000000000000000005101516124654200236710ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later // Style // Layout .inspection-detail { th { width: 16em; } pre, td { white-space: break-spaces; word-break: break-word; -webkit-hyphens: auto; -ms-hyphens: auto; hyphens: auto; } } icingadb-web-1.4.0/public/css/widget/object-meta-info.less000066400000000000000000000030231516124654200233770ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later // Style .object-meta-info { .vertical-key-value .value { font-weight: normal; } .vertical-key-value:first-child { text-align: left; } .vertical-key-value:last-child { text-align: right; } } .object-meta-info-control { .link-button(); .rounded-corners(0 0 .5em .5em); border: 1px solid; border-color: @gray-lighter; border-top-width: 0; background: @body-bg-color; } // Layout .object-meta-info { display: flex; justify-content: space-between; .horizontal-key-value { padding: 0; .key { width: 9em; } } .vertical-key-value { &:first-child { .text-ellipsis(); margin-right: 1em; } &:last-child { white-space: nowrap; margin-left: 1em; } } .horizontal-key-value .value, .horizontal-key-value .key, .vertical-key-value .value, .vertical-key-value .key { font-size: 16/18em; line-height: 18/16; // compensate smaller font-size (1/font-size) } } .object-meta-info-control { position: absolute; right: 2.25em; bottom: -2.5em; // height + margin-bottom height: 2em; padding: .25em; .collapse-icon, .expand-icon { font-size: 1.2em; &:before { margin-right: 0; } } .collapse-icon { display: block; } .expand-icon { display: none; } } .collapsed + .object-meta-info-control { .collapse-icon { display: none; } .expand-icon { display: block; } } icingadb-web-1.4.0/public/css/widget/object-statistics.less000066400000000000000000000014411516124654200237140ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later ul.object-statistics { // Reset defaults list-style-type: none; margin: 0; padding: 0; display: flex; align-items: center; > li:not(:last-child) { margin-right: 1em; } } .object-statistics-graph .donut-graph { height: 2em; width: 2em; vertical-align: middle; } .object-statistics-total a { display: inline-block; line-height: 1; position: relative; &:hover:before { .rounded-corners(); background-color: @gray-lighter; content: ""; display: block; z-index: 1; position: absolute; bottom: -.4em; left: -.4em; top: -.4em; right: -.4em; } .vertical-key-value { position: relative; z-index: 2; } } icingadb-web-1.4.0/public/css/widget/performance-data-table.less000066400000000000000000000017621516124654200245610ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2021 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .performance-data-table { width: 100%; overflow-x: auto; display: block; tr:not(:last-child) { border-bottom: 1px solid @gray-lighter; } td { text-align: right; .text-ellipsis(); } th { font-size: .857em; font-weight: normal; text-transform: uppercase; letter-spacing: .05em; } thead { border-bottom: 1px solid @gray-light; } th:first-child, td:first-child { padding-left: 0; } .title { text-align: left; width: 100%; } td.title { font-weight: bold; } .sparkline-col { min-width: 1.75em; width: 1.75em; padding: 2/12em 0; .invalid-perfdata { font-size: 1.25em; vertical-align: text-bottom; color: @color-warning; } } .inline-pie > svg { vertical-align: middle; } .no-value { color: @gray-semilight; text-align: center; display: block; } } icingadb-web-1.4.0/public/css/widget/quick-actions.less000066400000000000000000000015201516124654200230260ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .quick-actions { display: flex; flex-wrap: wrap; list-style-type: none; margin: 0 -.5em; padding: 0; a { text-decoration: none; } a, button { padding: .25em; .rounded-corners(); display: inline-flex; align-items: baseline; &:hover { background: @gray-lighter; } } li { margin: 0 .25em .5em .25em; vertical-align: middle; white-space: nowrap; } } .controls:not(.default-layout) > .quick-actions:last-child, .controls > .quick-actions:last-child { margin-bottom: 0; } #layout.twocols:not(.wide-layout) { .quick-actions { justify-content: space-between; min-width: 100%; } } #layout.wide-layout .controls { .quick-actions { float: left; } } icingadb-web-1.4.0/public/css/widget/service-state-badges.less000066400000000000000000000002341516124654200242560ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .service-state-badges { .state-badges(); } icingadb-web-1.4.0/public/css/widget/state-change.less000066400000000000000000000045761516124654200226350ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .state-change { display: inline-flex; &.reversed-state-balls { // This is needed, because with ~ we can address only subsequent nodes flex-direction: row-reverse; } .state-ball { .box-shadow(0, 0, 0, 1px, @body-bg-color); } // Same on same .state-ball ~ .state-ball { &.ball-size-xs { margin-left: -.05em; } &.ball-size-s { margin-left: -.15em; } &.ball-size-m { margin-left: -.275em; } &.ball-size-ml { margin-left: -.375em; } &.ball-size-l, &.ball-size-xl { margin-left: -.875em; } } // big left, smaller right &:not(.reversed-state-balls) .ball-size-l ~ .state-ball { &.ball-size-ml { margin-top: .25em; margin-left: -.5em; margin-right: .25em; } } // smaller left, big right &.reversed-state-balls .ball-size-l ~ .state-ball { &.ball-size-ml { z-index: -1; margin-top: .25em; margin-right: -.5em; } } .state-ball.state-ok, .state-ball.state-up, .state-pending { &.ball-size-l, &.ball-size-xl { background-color: @body-bg-color; } } // Avoid transparency on overlapping solid state-change state-balls .state-ball.handled { position: relative; opacity: 1; i { position: relative; z-index: 3; } &:before { content: ""; display: block; position: absolute; top: 0; right: 0; bottom: 0; left: 0; border-radius: 50%; opacity: .6; z-index: 2 } &:after { content: ""; display: block; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; border-radius: 50%; background-color: @body-bg-color; z-index: 1; } &.state-pending:before { background-color: @color-pending; } &.state-down:before { background-color: @color-down; } &.state-warning:before { background-color: @color-warning; } &.state-critical:before { background-color: @color-critical; } &.state-unknown:before { background-color: @color-unknown; } } } .overdue .state-change .state-ball { .box-shadow(0, 0, 0, 1px, @gray-lighter); &.handled:after { background-color: @gray-lighter; } } icingadb-web-1.4.0/public/css/widget/tag-list.less000066400000000000000000000013461516124654200220060ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .tag-list { line-height: 1.5; list-style-type: none; margin: -.25em 0 0 0; // TODO: This is wrong here, if at all this must be part of wherever this widget is placed padding: 0; > li { display: inline-block; &:not(:last-child) { margin-right: .417em; margin-bottom: .25em; // TODO: Really? It's an inline ul, bottom margin is outer layout.. } i { opacity: 0.8; } } > li > a { .rounded-corners(); background-color: @gray-lighter; display: block; padding: .25em .5em; &:hover { background-color: @gray-light; text-decoration: none; } } } icingadb-web-1.4.0/public/css/widget/view-mode-switcher.less000066400000000000000000000016251516124654200240040ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2020 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later .view-mode-switcher { display: flex; input { display: none; } label { color: @control-color; line-height: 1; background: @low-sat-blue; padding: 14/16*.25em 14/16*.5em; font-size: 16/12em; height: 24/16em; // desired pixel height / font-size cursor: pointer; &:first-of-type { border-top-left-radius: 0.25em; border-bottom-left-radius: 0.25em; } &:last-of-type { border-top-right-radius: 0.25em; border-bottom-right-radius: 0.25em; } &:not(:last-of-type) { border-right: 1px solid @low-sat-blue-dark; } i { // fix height for Chrome display: block; } } input[checked] + label { background-color: @control-color; color: @text-color-on-icinga-blue; cursor: default; } } icingadb-web-1.4.0/public/js/000077500000000000000000000000001516124654200157275ustar00rootroot00000000000000icingadb-web-1.4.0/public/js/action-list.js000066400000000000000000000744731516124654200205320ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2019 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later ;(function (Icinga) { "use strict"; try { var notjQuery = require('icinga/icinga-php-library/notjQuery'); } catch (e) { console.warn('Unable to provide input enrichments. Libraries not available:', e); return; } Icinga.Behaviors = Icinga.Behaviors || {}; class ActionList extends Icinga.EventListener { constructor(icinga) { super(icinga); this.on('click', '.action-list [data-action-item]:not(.page-separator), .action-list [data-action-item] a[href]', this.onClick, this); this.on('close-column', '#main > #col2', this.onColumnClose, this); this.on('column-moved', this.onColumnMoved, this); this.on('rendered', '#main .container', this.onRendered, this); this.on('keydown', '#body', this.onKeyDown, this); this.on('click', '.load-more[data-no-icinga-ajax] a', this.onLoadMoreClick, this); this.on('keypress', '.load-more[data-no-icinga-ajax] a', this.onKeyPress, this); this.lastActivatedItemUrl = null; this.lastTimeoutId = null; this.isProcessingLoadMore = false; this.activeRequests = {}; } /** * Parse the filter query contained in the given URL query string * * @param {string} queryString * * @returns {array} */ parseSelectionQuery(queryString) { return queryString.split('|'); } /** * Suspend auto refresh for the given item's container * * @param {Element} item * * @return {string} The container's id */ suspendAutoRefresh(item) { const container = item.closest('.container'); container.dataset.suspendAutorefresh = ''; return container.id; } /** * Enable auto refresh on the given container * * @param {string} containerId */ enableAutoRefresh(containerId) { delete document.getElementById(containerId).dataset.suspendAutorefresh; } onClick(event) { let _this = event.data.self; let target = event.currentTarget; if (target.matches('a') && (! target.matches('.subject') || event.ctrlKey || event.metaKey)) { return true; } event.preventDefault(); event.stopImmediatePropagation(); event.stopPropagation(); let item = target.closest('[data-action-item]'); let list = target.closest('.action-list'); let activeItems = _this.getActiveItems(list); let toActiveItems = [], toDeactivateItems = []; const isBeingMultiSelected = list.matches('[data-icinga-multiselect-url]') && (event.ctrlKey || event.metaKey || event.shiftKey); if (isBeingMultiSelected) { if (event.ctrlKey || event.metaKey) { if (item.classList.contains('active')) { toDeactivateItems.push(item); } else { toActiveItems.push(item); } } else { document.getSelection().removeAllRanges(); let allItems = _this.getAllItems(list); let startIndex = allItems.indexOf(item); if(startIndex < 0) { startIndex = 0; } let endIndex = activeItems.length ? allItems.indexOf(activeItems[0]) : 0; if (startIndex > endIndex) { toActiveItems = allItems.slice(endIndex, startIndex + 1); } else { endIndex = activeItems.length ? allItems.indexOf(activeItems[activeItems.length - 1]) : 0; toActiveItems = allItems.slice(startIndex, endIndex + 1); } toDeactivateItems = activeItems.filter(item => ! toActiveItems.includes(item)); toActiveItems = toActiveItems.filter(item => ! activeItems.includes(item)); } } else { toDeactivateItems = activeItems; toActiveItems.push(item); } if (activeItems.length === 1 && toActiveItems.length === 0 && _this.icinga.loader.getLinkTargetFor($(target)).attr('id') === 'col2' ) { _this.icinga.ui.layout1col(); _this.icinga.history.pushCurrentState(); _this.enableAutoRefresh('col1'); return; } let dashboard = list.closest('.dashboard'); if (dashboard) { _this.clearDashboardSelections(dashboard, list); } let lastActivatedUrl = null; if (toActiveItems.includes(item)) { lastActivatedUrl = item.dataset.icingaDetailFilter; } else if (activeItems.length > 1) { lastActivatedUrl = activeItems[activeItems.length - 1] === item ? activeItems[activeItems.length - 2].dataset.icingaDetailFilter : activeItems[activeItems.length - 1].dataset.icingaDetailFilter; } _this.clearSelection(toDeactivateItems); _this.setActive(toActiveItems); _this.addSelectionCountToFooter(list); _this.setLastActivatedItemUrl(lastActivatedUrl); _this.loadDetailUrl(list, target.matches('a') ? target.getAttribute('href') : null); } /** * Add the selection count to footer if list allow multi selection * * @param list */ addSelectionCountToFooter(list) { if (! list.matches('[data-icinga-multiselect-url]') || list.closest('.dashboard')) { return; } let activeItemCount = this.getActiveItems(list).length; let footer = list.closest('.container').querySelector('.footer'); // For items that do not have a bottom status bar like Downtimes, Comments... if (footer === null) { footer = notjQuery.render( '' ) list.closest('.container').appendChild(footer); } let selectionCount = footer.querySelector('.selection-count'); if (selectionCount === null) { selectionCount = notjQuery.render( '
' ); footer.prepend(selectionCount); } let selectedItems = selectionCount.querySelector('.selected-items'); selectedItems.innerText = activeItemCount ? list.dataset.icingaMultiselectCountLabel.replace('%d', activeItemCount) : list.dataset.icingaMultiselectHintLabel; if (activeItemCount === 0) { selectedItems.classList.add('hint'); } else { selectedItems.classList.remove('hint'); } } /** * Key navigation for .action-list * * Only for primary lists (dashboard or lists in detail view are not taken into account) * * - `Shift + ArrowUp|ArrowDown` = Multiselect * - `ArrowUp|ArrowDown` = Select next/previous * - `Ctrl|cmd + A` = Select all on currect page * * @param event */ onKeyDown(event) { let _this = event.data.self; let list = null; let pressedArrowDownKey = event.key === 'ArrowDown'; let pressedArrowUpKey = event.key === 'ArrowUp'; let focusedElement = document.activeElement; if ( _this.isProcessingLoadMore || ! event.key // input auto-completion is triggered || (event.key.toLowerCase() !== 'a' && ! pressedArrowDownKey && ! pressedArrowUpKey) ) { return; } if (focusedElement && ( focusedElement.matches('#main > :scope') || focusedElement.matches('#body')) ) { let activeItem = document.querySelector( '#main > .container > .content > .action-list [data-action-item].active' ); if (activeItem) { list = activeItem.closest('.action-list'); } else { list = focusedElement.querySelector('#main > .container > .content > .action-list'); } } else if (focusedElement) { list = focusedElement.closest('.content > .action-list'); } if (! list) { return; } let isMultiSelectableList = list.matches('[data-icinga-multiselect-url]'); if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') { if (! isMultiSelectableList) { return; } event.preventDefault(); _this.selectAll(list); return; } event.preventDefault(); let allItems = _this.getAllItems(list); let firstListItem = allItems[0]; let lastListItem = allItems[allItems.length -1]; let activeItems = _this.getActiveItems(list); let markAsLastActive = null; // initialized only if it is different from toActiveItem let toActiveItem = null; let wasAllSelected = activeItems.length === allItems.length; let lastActivatedItem = list.querySelector( `[data-icinga-detail-filter="${ _this.lastActivatedItemUrl }"]` ); if (! lastActivatedItem && activeItems.length) { lastActivatedItem = activeItems[activeItems.length - 1]; } let directionalNextItem = _this.getDirectionalNext(lastActivatedItem, event.key); if (activeItems.length === 0) { toActiveItem = pressedArrowDownKey ? firstListItem : lastListItem; // reset all on manual page refresh _this.clearSelection(activeItems); if (toActiveItem.classList.contains('load-more')) { toActiveItem = toActiveItem.previousElementSibling; } } else if (isMultiSelectableList && event.shiftKey) { if (activeItems.length === 1) { toActiveItem = directionalNextItem; } else if (wasAllSelected && ( (lastActivatedItem !== firstListItem && pressedArrowDownKey) || (lastActivatedItem !== lastListItem && pressedArrowUpKey) )) { if (pressedArrowDownKey) { toActiveItem = lastActivatedItem === lastListItem ? null : lastListItem; } else { toActiveItem = lastActivatedItem === firstListItem ? null : lastListItem; } } else if (directionalNextItem && directionalNextItem.classList.contains('active')) { // deactivate last activated by down to up select _this.clearSelection([lastActivatedItem]); if (wasAllSelected) { _this.scrollItemIntoView(lastActivatedItem, event.key); } toActiveItem = directionalNextItem; } else { [toActiveItem, markAsLastActive] = _this.findToActiveItem(lastActivatedItem, event.key); } } else { toActiveItem = directionalNextItem ?? lastActivatedItem; if (toActiveItem) { if (toActiveItem.classList.contains('load-more')) { clearTimeout(_this.lastTimeoutId); _this.handleLoadMoreNavigate(toActiveItem, lastActivatedItem, event.key); return; } _this.clearSelection(activeItems); if (toActiveItem.classList.contains('page-separator')) { toActiveItem = _this.getDirectionalNext(toActiveItem, event.key); } } } if (! toActiveItem) { return; } _this.setActive(toActiveItem); _this.setLastActivatedItemUrl( markAsLastActive ? markAsLastActive.dataset.icingaDetailFilter : toActiveItem.dataset.icingaDetailFilter ); _this.scrollItemIntoView(toActiveItem, event.key); _this.addSelectionCountToFooter(list); _this.loadDetailUrl(list); } /** * Get the next list item according to the pressed key (`ArrowUp` or `ArrowDown`) * * @param item The list item from which we want the next item * @param eventKey Pressed key (`ArrowUp` or `ArrowDown`) * * @returns {Element|null} */ getDirectionalNext(item, eventKey) { if (! item) { return null; } return eventKey === 'ArrowUp' ? item.previousElementSibling : item.nextElementSibling; } /** * Find the list item that should be activated next * * @param lastActivatedItem * @param eventKey Pressed key (`ArrowUp` or `ArrowDown`) * * @returns {Element[]} */ findToActiveItem(lastActivatedItem, eventKey) { let toActiveItem; let markAsLastActive; toActiveItem = this.getDirectionalNext(lastActivatedItem, eventKey); while (toActiveItem) { if (! toActiveItem.classList.contains('active')) { break; } toActiveItem = this.getDirectionalNext(toActiveItem, eventKey); } markAsLastActive = toActiveItem; // if the next/previous sibling element is already active, // mark the last/first active element in list as last active while (markAsLastActive && this.getDirectionalNext(markAsLastActive, eventKey)) { if (! this.getDirectionalNext(markAsLastActive, eventKey).classList.contains('active')) { break; } markAsLastActive = this.getDirectionalNext(markAsLastActive, eventKey); } return [toActiveItem, markAsLastActive]; } /** * Select All list items * * @param list The action list */ selectAll(list) { let allItems = this.getAllItems(list); let activeItems = this.getActiveItems(list); this.setActive(allItems.filter(item => ! activeItems.includes(item))); this.setLastActivatedItemUrl(allItems[allItems.length -1].dataset.icingaDetailFilter); this.addSelectionCountToFooter(list); this.loadDetailUrl(list); } /** * Clear the selection by removing .active class * * @param selectedItems The items with class active */ clearSelection(selectedItems) { selectedItems.forEach(item => item.classList.remove('active')); } /** * Set the last activated item Url * * @param url */ setLastActivatedItemUrl (url) { this.lastActivatedItemUrl = url; } /** * Scroll the given item into view * * @param item Item to scroll into view * @param pressedKey Pressed key (`ArrowUp` or `ArrowDown`) */ scrollItemIntoView(item, pressedKey) { let directionalNext = this.getDirectionalNext(item, pressedKey); if ("isDisplayContents" in item.parentElement.dataset) { item = item.firstChild; directionalNext = directionalNext ? directionalNext.firstChild : null; } // required when ArrowUp is pressed in new list OR after selecting all items with ctrl+A item.scrollIntoView({block: "nearest"}); if (directionalNext) { directionalNext.scrollIntoView({block: "nearest"}); } } clearDashboardSelections(dashboard, currentList) { dashboard.querySelectorAll('.action-list').forEach(otherList => { if (otherList !== currentList) { this.clearSelection(this.getActiveItems(otherList)); } }) } /** * Load the detail url with selected items * * @param list The action list * @param anchorUrl If any anchor is clicked (e.g. host in service list) */ loadDetailUrl(list, anchorUrl = null) { let url = anchorUrl; let activeItems = this.getActiveItems(list); if (url === null) { if (activeItems.length > 1) { url = this.createMultiSelectUrl(activeItems); } else { let anchor = activeItems[0].querySelector('[href]'); url = anchor ? anchor.getAttribute('href') : null; } } if (url === null) { return; } const suspendedContainer = this.suspendAutoRefresh(list); clearTimeout(this.lastTimeoutId); this.lastTimeoutId = setTimeout(() => { const requestNo = this.lastTimeoutId; this.activeRequests[requestNo] = suspendedContainer; this.lastTimeoutId = null; let req = this.icinga.loader.loadUrl( url, this.icinga.loader.getLinkTargetFor($(activeItems[0])) ); req.always((_, __, errorThrown) => { if (errorThrown !== 'abort') { this.enableAutoRefresh(this.activeRequests[requestNo]); } delete this.activeRequests[requestNo]; }); }, 250); } /** * Add .active class to given list item * * @param toActiveItem The list item(s) */ setActive(toActiveItem) { if (toActiveItem instanceof HTMLElement) { toActiveItem.classList.add('active'); } else { toActiveItem.forEach(item => item.classList.add('active')); } } /** * Get the active items from given list * * @param list The action list * * @return array */ getActiveItems(list) { let items; if (list.tagName.toLowerCase() === 'table') { items = list.querySelectorAll(':scope > tbody > [data-action-item].active'); } else { items = list.querySelectorAll(':scope > [data-action-item].active'); } return Array.from(items); } /** * Get all available items from given list * * @param list The action list * * @return array */ getAllItems(list) { let items; if (list.tagName.toLowerCase() === 'table') { items = list.querySelectorAll(':scope > tbody > [data-action-item]'); } else { items = list.querySelectorAll(':scope > [data-action-item]'); } return Array.from(items); } /** * Handle the navigation on load-more button * * @param loadMoreElement * @param lastActivatedItem * @param pressedKey Pressed key (`ArrowUp` or `ArrowDown`) */ handleLoadMoreNavigate(loadMoreElement, lastActivatedItem, pressedKey) { let req = this.loadMore(loadMoreElement.firstChild); this.isProcessingLoadMore = true; req.done(() => { this.isProcessingLoadMore = false; // list has now new items, so select the lastActivatedItem and then move forward let toActiveItem = lastActivatedItem.nextElementSibling; while (toActiveItem) { if (toActiveItem.hasAttribute('data-action-item')) { this.clearSelection([lastActivatedItem]); this.setActive(toActiveItem); this.setLastActivatedItemUrl(toActiveItem.dataset.icingaDetailFilter); this.scrollItemIntoView(toActiveItem, pressedKey); this.addSelectionCountToFooter(toActiveItem.parentElement); this.loadDetailUrl(toActiveItem.parentElement); return; } toActiveItem = toActiveItem.nextElementSibling; } }); } /** * Click on load-more button * * @param event * * @returns {boolean} */ onLoadMoreClick(event) { event.stopPropagation(); event.preventDefault(); event.data.self.loadMore(event.target); return false; } onKeyPress(event) { if (event.key === ' ') { // space event.data.self.onLoadMoreClick(event); } } /** * Load more list items based on the given anchor * * @param anchor * * @returns {*|{getAllResponseHeaders: function(): *|null, abort: function(*): this, setRequestHeader: function(*, *): this, readyState: number, getResponseHeader: function(*): null|*, overrideMimeType: function(*): this, statusCode: function(*): this}|jQuery|boolean} */ loadMore(anchor) { let showMore = anchor.parentElement; var progressTimer = this.icinga.timer.register(function () { var label = anchor.innerText; var dots = label.substr(-3); if (dots.slice(0, 1) !== '.') { dots = '. '; } else { label = label.slice(0, -3); if (dots === '...') { dots = '. '; } else if (dots === '.. ') { dots = '...'; } else if (dots === '. ') { dots = '.. '; } } anchor.innerText = label + dots; }, null, 250); let url = anchor.getAttribute('href'); let req = this.icinga.loader.loadUrl( // Add showCompact, we don't want controls in paged results this.icinga.utils.addUrlFlag(url, 'showCompact'), $(showMore.parentElement), undefined, undefined, 'append', false, progressTimer ); req.addToHistory = false; req.done(function () { showMore.remove(); // Set data-icinga-url to make it available for Icinga.History.getCurrentState() req.$target.closest('.container').data('icingaUrl', url); this.icinga.history.replaceCurrentState(); }); return req; } /** * Create the detail url for multi selectable list * * @param items List items * @param withBaseUrl Default to true * * @returns {string} The url */ createMultiSelectUrl(items, withBaseUrl = true) { let filters = []; items.forEach(item => { filters.push(item.getAttribute('data-icinga-multiselect-filter')); }); let url = '?' + filters.join('|'); if (withBaseUrl) { return items[0].closest('.action-list').getAttribute('data-icinga-multiselect-url') + url; } return url; } onColumnClose(event) { let _this = event.data.self; let list = _this.findDetailUrlActionList(document.getElementById('col1')); if (list && list.matches('[data-icinga-multiselect-url], [data-icinga-detail-url]')) { _this.clearSelection(_this.getActiveItems(list)); _this.addSelectionCountToFooter(list); } } /** * Find the action list using the detail url * * @param {Element} container * * @return Element|null */ findDetailUrlActionList(container) { let detailUrl = this.icinga.utils.parseUrl( this.icinga.history.getCol2State().replace(/^#!/, '') ); let detailItem = container.querySelector( '[data-icinga-detail-filter="' + detailUrl.query.replace('?', '') + '"],' + '[data-icinga-multiselect-filter="' + detailUrl.query.split('|', 1).toString().replace('?', '') + '"]' ); return detailItem ? detailItem.parentElement : null; } /** * Triggers when column is moved to left or right * * @param event * @param sourceId The content is moved from */ onColumnMoved(event, sourceId) { let _this = event.data.self; if (event.target.id === 'col2' && sourceId === 'col1') { // only for browser-back (col1 shifted to col2) _this.clearSelection(event.target.querySelectorAll('.action-list .active')); } else if (event.target.id === 'col1' && sourceId === 'col2') { for (const requestNo of Object.keys(_this.activeRequests)) { if (_this.activeRequests[requestNo] === sourceId) { _this.enableAutoRefresh(_this.activeRequests[requestNo]); _this.activeRequests[requestNo] = _this.suspendAutoRefresh(event.target); } } } } onRendered(event, isAutoRefresh) { let _this = event.data.self; let container = event.target; let isTopLevelContainer = container.matches('#main > :scope'); let list; if (event.currentTarget !== container || Object.keys(_this.activeRequests).length) { // Nested containers are not processed multiple times || still processing selection/navigation request return; } else if (isTopLevelContainer && container.id !== 'col1') { if (isAutoRefresh) { return; } // only for browser back/forward navigation list = _this.findDetailUrlActionList(document.getElementById('col1')); } else { list = _this.findDetailUrlActionList(container); } if (! list || ! ("isDisplayContents" in list.dataset)) { // no detail view || ignore when already set let actionLists = null; if (! list) { actionLists = document.querySelectorAll('.action-list'); } else { actionLists = [list]; } for (let actionList of actionLists) { let firstSelectableItem = actionList.querySelector(':scope > [data-action-item]'); if ( firstSelectableItem && ( ! firstSelectableItem.checkVisibility() && firstSelectableItem.firstChild && firstSelectableItem.firstChild.checkVisibility() ) ) { actionList.dataset.isDisplayContents = ""; } } } if (list && list.matches('[data-icinga-multiselect-url], [data-icinga-detail-url]')) { let detailUrl = _this.icinga.utils.parseUrl( _this.icinga.history.getCol2State().replace(/^#!/, '') ); let toActiveItems = []; if (list.dataset.icingaMultiselectUrl === detailUrl.path) { for (const filter of _this.parseSelectionQuery(detailUrl.query.slice(1))) { let item = list.querySelector( '[data-icinga-multiselect-filter="' + filter + '"]' ); if (item) { toActiveItems.push(item); } } } else if (_this.matchesDetailUrl(list.dataset.icingaDetailUrl, detailUrl.path)) { let item = list.querySelector( '[data-icinga-detail-filter="' + detailUrl.query.slice(1) + '"]' ); if (item) { toActiveItems.push(item); } } let dashboard = list.closest('.dashboard'); if (dashboard) { _this.clearDashboardSelections(dashboard, list); } _this.clearSelection(_this.getAllItems(list).filter(item => !toActiveItems.includes(item))); _this.setActive(toActiveItems); } if (isTopLevelContainer) { let footerList = list ?? container.querySelector('.content > .action-list'); if (footerList) { _this.addSelectionCountToFooter(footerList); } } } matchesDetailUrl(itemUrl, detailUrl) { if (itemUrl === detailUrl) { return true; } // The slash is used to avoid false positives (e.g. icingadb/hostgroup and icingadb/host) return detailUrl.startsWith(itemUrl + '/'); } } Icinga.Behaviors.ActionList = ActionList; }(Icinga)); icingadb-web-1.4.0/public/js/progress-bar.js000066400000000000000000000072701516124654200207010ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 Icinga GmbH // SPDX-License-Identifier: GPL-3.0-or-later (function (Icinga) { "use strict"; class ProgressBar extends Icinga.EventListener { constructor(icinga) { super(icinga); /** * Frame update threshold. If it reaches zero, the view is updated * * Currently, only every third frame is updated. * * @type {number} */ this.frameUpdateThreshold = 3; /** * Threshold at which animations get smoothed out (in milliseconds) * * @type {number} */ this.smoothUpdateThreshold = 250; this.on('rendered', '#main > .container', this.onRendered, this); } onRendered(event) { const _this = event.data.self; const container = event.target; container.querySelectorAll('[data-animate-progress]').forEach(progress => { const frequency = ( (Number(progress.dataset.endTime) - Number(progress.dataset.startTime) ) * 1000) / progress.parentElement.offsetWidth; _this.updateProgress( now => _this.animateProgress(progress, now), frequency); }); } animateProgress(progress, now) { if (! progress.isConnected) { return false; // Exit early if the node is removed from the DOM } const durationScale = 100; const startTime = Number(progress.dataset.startTime); const endTime = Number(progress.dataset.endTime); const duration = endTime - startTime; const end = new Date(endTime * 1000.0); let leftNow = durationScale * (1 - (end - now) / (duration * 1000.0)); if (leftNow > durationScale) { leftNow = durationScale; } else if (leftNow < 0) { leftNow = 0; } const switchAfter = Number(progress.dataset.switchAfter); if (! isNaN(switchAfter)) { const switchClass = progress.dataset.switchClass; const switchAt = new Date((startTime * 1000.0) + (switchAfter * 1000.0)); if (now < switchAt) { progress.classList.add(switchClass); } else if (progress.classList.contains(switchClass)) { progress.classList.remove(switchClass); } } const bar = progress.querySelector(':scope > .bar'); bar.style.width = leftNow + '%'; return leftNow !== durationScale; } updateProgress(callback, frequency, now = null) { if (now === null) { now = new Date(); } if (! callback(now)) { return; } if (frequency < this.smoothUpdateThreshold) { let counter = this.frameUpdateThreshold; const onNextFrame = timeSinceOrigin => { if (--counter === 0) { this.updateProgress(callback, frequency, new Date(performance.timeOrigin + timeSinceOrigin)); } else { requestAnimationFrame(onNextFrame); } }; requestAnimationFrame(onNextFrame); } else { setTimeout(() => this.updateProgress(callback, frequency), frequency); } } } Icinga.Behaviors = Icinga.Behaviors || {}; Icinga.Behaviors.ProgressBar = ProgressBar; })(Icinga); icingadb-web-1.4.0/run.php000066400000000000000000000030541516124654200153540ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later /** @var $this \Icinga\Application\Modules\Module */ $this->provideHook('ApplicationState'); $this->provideHook('X509/Sni'); $this->provideHook('health', 'IcingaHealth'); $this->provideHook('health', 'RedisHealth'); $this->provideHook('Reporting/Report', 'Reporting/HostSlaReport'); $this->provideHook('Reporting/Report', 'Reporting/TotalHostSlaReport'); $this->provideHook('Reporting/Report', 'Reporting/ServiceSlaReport'); $this->provideHook('Reporting/Report', 'Reporting/TotalServiceSlaReport'); if ($this::exists('notifications')) { $this->provideHook('Notifications/v1/Source'); } if ($this::exists('reporting')) { $this->provideHook('Icingadb/HostActions', 'CreateHostSlaReport'); $this->provideHook('Icingadb/ServiceActions', 'CreateServiceSlaReport'); $this->provideHook('Icingadb/HostsDetailExtension', 'CreateHostsSlaReport'); $this->provideHook('Icingadb/ServicesDetailExtension', 'CreateServicesSlaReport'); } if (! $this::exists('monitoring') && $this->app->getModuleManager()->hasInstalled('monitoring')) { // For compatibility reasons, Icinga DB Web also supports hooks originally written for the monitoring module. // This requires the monitoring module to be either enabled or installed. // If it is only installed, its autoloader must be registered manually to resolve monitoring module hook classes. $this->app->getModuleManager()->getModule('monitoring', assertLoaded: false)->registerAutoloader(); } icingadb-web-1.4.0/test/000077500000000000000000000000001516124654200150145ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/000077500000000000000000000000001516124654200156035ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/Lib/000077500000000000000000000000001516124654200163115ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/Lib/FailoverCommandTransport.php000066400000000000000000000023641516124654200240120ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Module\Icingadb\Lib; use Icinga\Application\Config; use Icinga\Data\ConfigObject; use Icinga\Module\Icingadb\Command\IcingaApiCommand; use Icinga\Module\Icingadb\Command\Transport\ApiCommandTransport; use Icinga\Module\Icingadb\Command\Transport\CommandTransport; use Icinga\Module\Icingadb\Command\Transport\CommandTransportException; class FailoverCommandTransport extends CommandTransport { public static function getConfig(): Config { return Config::fromArray(['endpoint1' => ['host' => 'endpointA'], 'endpoint2' => ['host' => 'endpointB']]); } public static function createTransport(ConfigObject $config): ApiCommandTransport { return (new class extends ApiCommandTransport { protected function sendCommand(IcingaApiCommand $command) { if ($this->getHost() === 'endpointA') { throw (new CommandTransportException(sprintf('%s fails!', $this->getHost()))) ->setCommand($command); } return $command->getData(); } })->setHost($config->host); } } icingadb-web-1.4.0/test/php/Lib/IntermittentlyFailingCommandTransport.php000066400000000000000000000030551516124654200265660ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Module\Icingadb\Lib; use Icinga\Application\Config; use Icinga\Data\ConfigObject; use Icinga\Module\Icingadb\Command\IcingaApiCommand; use Icinga\Module\Icingadb\Command\Transport\ApiCommandTransport; use Icinga\Module\Icingadb\Command\Transport\CommandTransport; use Icinga\Module\Icingadb\Command\Transport\CommandTransportException; class IntermittentlyFailingCommandTransport extends CommandTransport { public static $failAtAttemptNo = 2; public static $attemptNo = 0; public static function getConfig(): Config { return Config::fromArray(['endpoint1' => ['host' => 'endpointA'], 'endpoint2' => ['host' => 'endpointB']]); } public static function createTransport(ConfigObject $config): ApiCommandTransport { return (new class extends ApiCommandTransport { protected function sendCommand(IcingaApiCommand $command) { $attemptNo = ++IntermittentlyFailingCommandTransport::$attemptNo; $failAtAttemptNo = IntermittentlyFailingCommandTransport::$failAtAttemptNo; if ($attemptNo === $failAtAttemptNo) { throw (new CommandTransportException(sprintf('%s intermittently fails!', $this->getHost()))) ->setCommand($command); } return $command->getData() + ['endpoint' => $this->getHost()]; } })->setHost($config->host); } } icingadb-web-1.4.0/test/php/Lib/PerfdataSetWithPublicData.php000066400000000000000000000004441516124654200240130ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Module\Icingadb\Lib; use Icinga\Module\Icingadb\Util\PerfDataSet; class PerfdataSetWithPublicData extends PerfdataSet { public $perfdata = []; } icingadb-web-1.4.0/test/php/Lib/StrikingCommandTransport.php000066400000000000000000000021121516124654200240240ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Module\Icingadb\Lib; use Icinga\Application\Config; use Icinga\Data\ConfigObject; use Icinga\Module\Icingadb\Command\IcingaApiCommand; use Icinga\Module\Icingadb\Command\Transport\ApiCommandTransport; use Icinga\Module\Icingadb\Command\Transport\CommandTransport; use Icinga\Module\Icingadb\Command\Transport\CommandTransportException; class StrikingCommandTransport extends CommandTransport { public static function getConfig(): Config { return Config::fromArray(['endpoint1' => ['host' => 'endpointA'], 'endpoint2' => ['host' => 'endpointB']]); } public static function createTransport(ConfigObject $config): ApiCommandTransport { return (new class extends ApiCommandTransport { protected function sendCommand(IcingaApiCommand $command) { throw new CommandTransportException(sprintf('%s strikes!', $this->getHost())); } })->setHost($config->host); } } icingadb-web-1.4.0/test/php/application/000077500000000000000000000000001516124654200201065ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/application/clicommands/000077500000000000000000000000001516124654200223775ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/application/clicommands/MigrateCommandTest.php000066400000000000000000002150351516124654200266450ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Module\Icingadb\Clicommands; use Icinga\Application\Cli; use Icinga\Application\Config; use Icinga\Application\Modules\Manager; use Icinga\Cli\Params; use Icinga\Data\ConfigObject; use Icinga\Exception\IcingaException; use Icinga\Exception\MissingParameterException; use Icinga\File\Storage\TemporaryLocalFileStorage; use Icinga\Module\Icingadb\Clicommands\MigrateCommand; use PHPUnit\Framework\TestCase; class MigrateCommandTest extends TestCase { protected $config = [ 'dashboards' => [ 'initial' => [ 'hosts' => [ 'title' => 'Hosts' ], 'hosts.problems' => [ 'title' => 'Host Problems', 'url' => 'monitoring/list/hosts?host_problem=1' ], 'hosts.group_members' => [ 'title' => 'Group Members', 'url' => 'monitoring/list/hosts?hostgroup_name=group1|hostgroup_name=%28group2%29' ], 'hosts.variables' => [ 'title' => 'Host Variables', 'url' => 'monitoring/list/hosts?(_host_foo=bar&_host_bar=foo)|_host_rab=oof' ], 'hosts.wildcards' => [ 'title' => 'Host Wildcards', 'url' => 'monitoring/list/hosts?host_name=%2Afoo%2A|host_name=%2Abar%2A' . '&sort=host_severity&dir=asc&limit=25' ], 'hosts.encoded_params' => [ 'title' => 'Host Encoded Params', 'url' => 'monitoring/list/hosts?host_name=%28foo%29&sort=_host_%28foo%29' ], 'icingadb' => [ 'title' => 'Icinga DB' ], 'icingadb.no-wildcards' => [ 'title' => 'No Wildcards', 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name=linux-hosts' ], 'icingadb.wildcards' => [ 'title' => 'Wildcards', 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name=%2Alinux%2A' ], 'icingadb.also-wildcards' => [ 'title' => 'Also Wildcards', 'url' => 'icingadb/hosts?host.name=%2Afoo%2A' ], 'icingadb.with-sort-and-limit' => [ 'title' => 'With Sort And Limit', 'url' => 'icingadb/hosts?host.name=%2Afoo%2A|host.name=bar&sort=host.state.severity&limit=50' ], 'not-monitoring-or-icingadb' => [ 'title' => 'Not Monitoring Or Icinga DB' ], 'not-monitoring-or-icingadb.something' => [ 'title' => 'Something', 'url' => 'somewhere/something?foo=%2Abar%2A' ] ], 'expected' => [ 'hosts' => [ 'title' => 'Hosts' ], 'hosts.problems' => [ 'title' => 'Host Problems', 'url' => 'icingadb/hosts?host.state.is_problem=y' ], 'hosts.group_members' => [ 'title' => 'Group Members', 'url' => 'icingadb/hosts?hostgroup.name=group1|hostgroup.name=%28group2%29' ], 'hosts.variables' => [ 'title' => 'Host Variables', 'url' => 'icingadb/hosts?(host.vars.foo=bar&host.vars.bar=foo)|host.vars.rab=oof' ], 'hosts.wildcards' => [ 'title' => 'Host Wildcards', 'url' => 'icingadb/hosts?host.name~%2Afoo%2A|host.name~%2Abar%2A' . '&sort=host.state.severity%20asc&limit=25' ], 'hosts.encoded_params' => [ 'title' => 'Host Encoded Params', 'url' => 'icingadb/hosts?host.name=%28foo%29&sort=host.vars.%28foo%29' ], 'icingadb' => [ 'title' => 'Icinga DB' ], 'icingadb.no-wildcards' => [ 'title' => 'No Wildcards', 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name=linux-hosts' ], 'icingadb.wildcards' => [ 'title' => 'Wildcards', 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name~%2Alinux%2A' ], 'icingadb.also-wildcards' => [ 'title' => 'Also Wildcards', 'url' => 'icingadb/hosts?host.name~%2Afoo%2A' ], 'icingadb.with-sort-and-limit' => [ 'title' => 'With Sort And Limit', 'url' => 'icingadb/hosts?host.name~%2Afoo%2A|host.name=bar&sort=host.state.severity&limit=50' ], 'not-monitoring-or-icingadb' => [ 'title' => 'Not Monitoring Or Icinga DB' ], 'not-monitoring-or-icingadb.something' => [ 'title' => 'Something', 'url' => 'somewhere/something?foo=%2Abar%2A' ] ] ], 'menu-items' => [ 'initial' => [ 'foreign-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'example.com?q=foo' ], 'monitoring-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'monitoring/list/hosts?host_problem=1' ], 'icingadb-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'icingadb/hosts?host.name=%2Afoo%2A' ] ], 'expected' => [ 'foreign-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'example.com?q=foo' ], 'monitoring-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'icingadb/hosts?host.state.is_problem=y' ], 'icingadb-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'icingadb/hosts?host.name~%2Afoo%2A' ] ] ], 'shared-menu-items' => [ 'initial' => [ 'foreign-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'example.com?q=foo', 'owner' => 'test' ], 'monitoring-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'monitoring/list/hosts?host_problem=1', 'owner' => 'test' ], 'icingadb-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'icingadb/hosts?host.name=%2Afoo%2A', 'owner' => 'test' ], 'other-monitoring-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'monitoring/list/hosts?host_problem=1', 'owner' => 'not-test' ] ], 'expected' => [ 'foreign-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'example.com?q=foo', 'owner' => 'test' ], 'monitoring-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'icingadb/hosts?host.state.is_problem=y', 'owner' => 'test' ], 'icingadb-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'icingadb/hosts?host.name~%2Afoo%2A', 'owner' => 'test' ], 'other-monitoring-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'monitoring/list/hosts?host_problem=1', 'owner' => 'not-test' ] ] ], 'host-actions' => [ 'initial' => [ 'hosts' => [ 'type' => 'host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host_name=%2Afoo%2A' ], 'hosts_encoded_params' => [ 'type' => 'host-action', 'url' => 'monitoring/list/hosts?host_name=%28foo%29&sort=_host_%28foo%29', 'filter' => '_host_%28foo%29=bar' ] ], 'expected' => [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name~%2Afoo%2A' ], 'hosts_encoded_params' => [ 'type' => 'icingadb-host-action', 'url' => 'icingadb/hosts?host.name=%28foo%29&sort=host.vars.%28foo%29', 'filter' => 'host.vars.%28foo%29=bar' ] ] ], 'icingadb-host-actions' => [ 'initial' => [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name=%2Afoo%2A' ] ], 'expected' => [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name~%2Afoo%2A' ] ] ], 'service-actions' => [ 'initial' => [ 'services' => [ 'type' => 'service-action', 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', 'filter' => '_service_foo=bar&_service_bar=%2Afoo%2A' ], 'services_encoded_params' => [ 'type' => 'host-action', 'url' => 'monitoring/list/services?host_name=%28foo%29&sort=_host_%28foo%29', 'filter' => '_host_%28foo%29=bar' ] ], 'expected' => [ 'services' => [ 'type' => 'icingadb-service-action', 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', 'filter' => 'service.vars.foo=bar&service.vars.bar~%2Afoo%2A' ], 'services_encoded_params' => [ 'type' => 'icingadb-host-action', 'url' => 'icingadb/services?host.name=%28foo%29&sort=host.vars.%28foo%29', 'filter' => 'host.vars.%28foo%29=bar' ] ] ], 'icingadb-service-actions' => [ 'initial' => [ 'services' => [ 'type' => 'icingadb-service-action', 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', 'filter' => 'service.vars.foo=%2Abar%2A' ] ], 'expected' => [ 'services' => [ 'type' => 'icingadb-service-action', 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', 'filter' => 'service.vars.foo~%2Abar%2A' ] ] ], 'shared-host-actions' => [ 'initial' => [ 'shared-hosts' => [ 'type' => 'host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host_name=%2Afoo%2A', 'owner' => 'test' ], 'hosts_encoded_params' => [ 'type' => 'host-action', 'url' => 'monitoring/list/hosts?host_name=%28foo%29&sort=_host_%28foo%29', 'filter' => '_host_%28foo%29=bar', 'owner' => 'test' ], 'other-hosts' => [ 'type' => 'host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host_name=%2Afoo%2A', 'owner' => 'not-test' ] ], 'expected' => [ 'shared-hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name~%2Afoo%2A', 'owner' => 'test' ], 'hosts_encoded_params' => [ 'type' => 'icingadb-host-action', 'url' => 'icingadb/hosts?host.name=%28foo%29&sort=host.vars.%28foo%29', 'filter' => 'host.vars.%28foo%29=bar', 'owner' => 'test' ] ] ], 'host-actions-legacy-macros' => [ 'initial' => [ 'hosts' => [ 'type' => 'host-action', 'url' => 'example.com/search?q=$HOSTNAME$,$HOSTADDRESS$,$HOSTADDRESS6$', 'filter' => 'host_name=%2Afoo%2A' ] ], 'expected' => [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$,$host.address$,$host.address6$', 'filter' => 'host.name~%2Afoo%2A' ] ] ], 'service-actions-legacy-macros' => [ 'initial' => [ 'services' => [ 'type' => 'service-action', 'url' => 'example.com/search?q=$SERVICEDESC$,$HOSTNAME$,$HOSTADDRESS$,$HOSTADDRESS6$', 'filter' => '_service_foo=bar&_service_bar=%2Afoo%2A' ] ], 'expected' => [ 'services' => [ 'type' => 'icingadb-service-action', 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', 'filter' => 'service.vars.foo=bar&service.vars.bar~%2Afoo%2A' ] ] ], 'all-roles' => [ 'initial' => [ 'no-wildcards' => [ 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'wildcards' => [ 'monitoring/filter/objects' => 'host_name=%2Afoo%2A|hostgroup_name=%2Afoo%2A' ], 'encoded_column' => [ 'monitoring/filter/objects' => '_host_%28foo%29=bar' ], 'blacklist' => [ 'monitoring/blacklist/properties' => 'host.vars.foo,service.vars.bar*,host.vars.a.**.d' ], 'full-access' => [ 'permissions' => 'module/monitoring,monitoring/*' ], 'general-read-access' => [ 'permissions' => 'module/monitoring' ], 'general-write-access' => [ 'permissions' => 'module/monitoring,monitoring/command/*' ], 'full-fine-grained-access' => [ 'permissions' => 'module/monitoring' . ',monitoring/command/schedule-check' . ',monitoring/command/acknowledge-problem' . ',monitoring/command/remove-acknowledgement' . ',monitoring/command/comment/add' . ',monitoring/command/comment/delete' . ',monitoring/command/downtime/schedule' . ',monitoring/command/downtime/delete' . ',monitoring/command/process-check-result' . ',monitoring/command/feature/instance' . ',monitoring/command/feature/object/active-checks' . ',monitoring/command/feature/object/passive-checks' . ',monitoring/command/feature/object/notifications' . ',monitoring/command/feature/object/event-handler' . ',monitoring/command/feature/object/flap-detection' . ',monitoring/command/send-custom-notification' ], 'full-with-refusals' => [ 'permissions' => 'module/monitoring,monitoring/command/*', 'refusals' => 'monitoring/command/downtime/*,monitoring/command/feature/instance' ], 'active-only' => [ 'permissions' => 'module/monitoring,monitoring/command/schedule-check/active-only' ], 'no-monitoring-contacts' => [ 'permissions' => 'module/monitoring,no-monitoring/contacts' ], 'reporting-only' => [ 'permissions' => 'module/reporting' ], 'icingadb' => [ 'icingadb/filter/objects' => 'host.name=%2Afoo%2A|hostgroup.name=%2Afoo%2A', 'icingadb/filter/services' => 'service.name=%2Abar%2A&service.vars.env=prod', 'icingadb/filter/hosts' => 'host.vars.env=%2Afoo%2A' ] ], 'expected' => [ 'no-wildcards' => [ 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo', 'icingadb/filter/objects' => 'host.name=foo|hostgroup.name=foo' ], 'wildcards' => [ 'monitoring/filter/objects' => 'host_name=%2Afoo%2A|hostgroup_name=%2Afoo%2A', 'icingadb/filter/objects' => 'host.name~%2Afoo%2A|hostgroup.name~%2Afoo%2A' ], 'encoded_column' => [ 'monitoring/filter/objects' => '_host_%28foo%29=bar', 'icingadb/filter/objects' => 'host.vars.%28foo%29=bar' ], 'blacklist' => [ 'monitoring/blacklist/properties' => 'host.vars.foo,service.vars.bar*,host.vars.a.**.d', 'icingadb/denylist/variables' => 'foo,bar*,a.*.d' ], 'full-access' => [ 'permissions' => 'module/monitoring,monitoring/*' ], 'general-read-access' => [ 'permissions' => 'module/monitoring' ], 'general-write-access' => [ 'permissions' => 'module/monitoring,monitoring/command/*,icingadb/command/*' ], 'full-fine-grained-access' => [ 'permissions' => 'module/monitoring' . ',monitoring/command/schedule-check' . ',icingadb/command/schedule-check' . ',monitoring/command/acknowledge-problem' . ',icingadb/command/acknowledge-problem' . ',monitoring/command/remove-acknowledgement' . ',icingadb/command/remove-acknowledgement' . ',monitoring/command/comment/add' . ',icingadb/command/comment/add' . ',monitoring/command/comment/delete' . ',icingadb/command/comment/delete' . ',monitoring/command/downtime/schedule' . ',icingadb/command/downtime/schedule' . ',monitoring/command/downtime/delete' . ',icingadb/command/downtime/delete' . ',monitoring/command/process-check-result' . ',icingadb/command/process-check-result' . ',monitoring/command/feature/instance' . ',icingadb/command/feature/instance' . ',monitoring/command/feature/object/active-checks' . ',icingadb/command/feature/object/active-checks' . ',monitoring/command/feature/object/passive-checks' . ',icingadb/command/feature/object/passive-checks' . ',monitoring/command/feature/object/notifications' . ',icingadb/command/feature/object/notifications' . ',monitoring/command/feature/object/event-handler' . ',icingadb/command/feature/object/event-handler' . ',monitoring/command/feature/object/flap-detection' . ',icingadb/command/feature/object/flap-detection' . ',monitoring/command/send-custom-notification' . ',icingadb/command/send-custom-notification' ], 'full-with-refusals' => [ 'permissions' => 'module/monitoring,monitoring/command/*,icingadb/command/*', 'refusals' => 'monitoring/command/downtime/*' . ',icingadb/command/downtime/*' . ',monitoring/command/feature/instance' . ',icingadb/command/feature/instance' ], 'active-only' => [ 'permissions' => 'module/monitoring' . ',monitoring/command/schedule-check/active-only' . ',icingadb/command/schedule-check/active-only' ], 'no-monitoring-contacts' => [ 'permissions' => 'module/monitoring,no-monitoring/contacts', 'icingadb/denylist/routes' => 'contacts,contactgroups' ], 'reporting-only' => [ 'permissions' => 'module/reporting' ], 'icingadb' => [ 'icingadb/filter/objects' => 'host.name~%2Afoo%2A|hostgroup.name~%2Afoo%2A', 'icingadb/filter/services' => 'service.name~%2Abar%2A&service.vars.env=prod', 'icingadb/filter/hosts' => 'host.vars.env~%2Afoo%2A' ] ] ], 'single-role-or-group' => [ 'initial' => [ 'one' => [ 'groups' => 'support,helpdesk', 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'two' => [ 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'three' => [ 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' ] ], 'expected' => [ 'one' => [ 'groups' => 'support,helpdesk', 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo', 'icingadb/filter/objects' => 'host.name=foo|hostgroup.name=foo' ], 'two' => [ 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'three' => [ 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' ] ] ] ]; protected $defaultConfigDir; protected $fileStorage; protected function setUp(): void { $this->defaultConfigDir = Config::$configDir; $this->fileStorage = new TemporaryLocalFileStorage(); Config::$configDir = dirname($this->fileStorage->resolvePath('bogus')); } protected function tearDown(): void { Config::$configDir = $this->defaultConfigDir; unset($this->fileStorage); // Should clean up automatically Config::module('monitoring', 'config', true); } protected function getConfig(string $case): array { return [$this->config[$case]['initial'], $this->config[$case]['expected']]; } protected function createConfig(string $path, array $data): void { $config = new Config(new ConfigObject($data)); $config->saveIni($this->fileStorage->resolvePath($path)); } protected function loadConfig(string $path): array { return Config::fromIni($this->fileStorage->resolvePath($path))->toArray(); } protected function createCommandInstance(string ...$params): MigrateCommand { array_unshift($params, 'program'); $app = $this->createConfiguredMock(Cli::class, [ 'getParams' => new Params($params), 'getModuleManager' => $this->createConfiguredMock(Manager::class, [ 'loadEnabledModules' => null ]) ]); return new MigrateCommand( $app, 'migrate', 'toicingadb', 'dashboard', false ); } /** * Checks the following: * - Whether only a single user is handled * - Whether backups are made * - Whether a second run changes nothing, if nothing changed * - Whether a second run keeps the backup, if nothing changed * - Whether a new backup isn't made, if nothing changed * - Whether existing Icinga DB dashboards are transformed regarding wildcard filters */ public function testDashboardMigrationBehavesAsExpectedByDefault() { [$initialConfig, $expected] = $this->getConfig('dashboards'); $this->createConfig('dashboards/test/dashboard.ini', $initialConfig); $this->createConfig('dashboards/test2/dashboard.ini', $initialConfig); $command = $this->createCommandInstance('--user', 'test'); $command->dashboardAction(); $config = $this->loadConfig('dashboards/test/dashboard.ini'); $this->assertSame($expected, $config); $config2 = $this->loadConfig('dashboards/test2/dashboard.ini'); $this->assertSame($initialConfig, $config2); $backup = $this->loadConfig('dashboards/test/dashboard.backup.ini'); $this->assertSame($initialConfig, $backup); $command = $this->createCommandInstance('--user', 'test'); $command->dashboardAction(); $configAfterSecondRun = $this->loadConfig('dashboards/test/dashboard.ini'); $this->assertSame($config, $configAfterSecondRun); $backupAfterSecondRun = $this->loadConfig('dashboards/test/dashboard.backup.ini'); $this->assertSame($backup, $backupAfterSecondRun); $backup1AfterSecondRun = $this->loadConfig('dashboards/test/dashboard.backup1.ini'); $this->assertEmpty($backup1AfterSecondRun); } /** * Checks the following: * - Whether a second run creates a new backup, if something changed */ public function testDashboardMigrationCreatesMultipleBackups() { $initialOldConfig = [ 'hosts' => [ 'title' => 'Hosts' ], 'hosts.problems' => [ 'title' => 'Host Problems', 'url' => 'monitoring/list/hosts?host_problem=1' ] ]; $initialNewConfig = [ 'hosts' => [ 'title' => 'Hosts' ], 'hosts.problems' => [ 'title' => 'Host Problems', 'url' => 'icingadb/hosts?host.state.is_problem=y' ], 'hosts.group_members' => [ 'title' => 'Group Members', 'url' => 'monitoring/list/hosts?hostgroup_name=group1|hostgroup_name=group2' ] ]; $expectedNewConfig = [ 'hosts' => [ 'title' => 'Hosts' ], 'hosts.problems' => [ 'title' => 'Host Problems', 'url' => 'icingadb/hosts?host.state.is_problem=y' ] ]; $expectedFinalConfig = [ 'hosts' => [ 'title' => 'Hosts' ], 'hosts.problems' => [ 'title' => 'Host Problems', 'url' => 'icingadb/hosts?host.state.is_problem=y' ], 'hosts.group_members' => [ 'title' => 'Group Members', 'url' => 'icingadb/hosts?hostgroup.name=group1|hostgroup.name=group2' ] ]; $this->createConfig('dashboards/test/dashboard.ini', $initialOldConfig); $command = $this->createCommandInstance('--user', 'test'); $command->dashboardAction(); $newConfig = $this->loadConfig('dashboards/test/dashboard.ini'); $this->assertSame($expectedNewConfig, $newConfig); $oldBackup = $this->loadConfig('dashboards/test/dashboard.backup.ini'); $this->assertSame($initialOldConfig, $oldBackup); $this->createConfig('dashboards/test/dashboard.ini', $initialNewConfig); $command = $this->createCommandInstance('--user', 'test'); $command->dashboardAction(); $finalConfig = $this->loadConfig('dashboards/test/dashboard.ini'); $this->assertSame($expectedFinalConfig, $finalConfig); $newBackup = $this->loadConfig('dashboards/test/dashboard.backup1.ini'); $this->assertSame($initialNewConfig, $newBackup); } /** * Checks the following: * - Whether backups are skipped * * @depends testDashboardMigrationBehavesAsExpectedByDefault */ public function testDashboardMigrationSkipsBackupIfRequested() { [$initialConfig, $expected] = $this->getConfig('dashboards'); $this->createConfig('dashboards/test/dashboard.ini', $initialConfig); $command = $this->createCommandInstance('--user', 'test', '--no-backup'); $command->dashboardAction(); $config = $this->loadConfig('dashboards/test/dashboard.ini'); $this->assertSame($expected, $config); $backup = $this->loadConfig('dashboards/test/dashboard.backup.ini'); $this->assertEmpty($backup); } /** * Checks the following: * - Whether multiple users are handled * - Whether multiple backups are made * * @depends testDashboardMigrationBehavesAsExpectedByDefault */ public function testDashboardMigrationMigratesAllUsers() { [$initialConfig, $expected] = $this->getConfig('dashboards'); $users = ['foo', 'bar', 'raboof']; foreach ($users as $user) { $this->createConfig("dashboards/$user/dashboard.ini", $initialConfig); } $command = $this->createCommandInstance('--user', '*'); $command->dashboardAction(); foreach ($users as $user) { $config = $this->loadConfig("dashboards/$user/dashboard.ini"); $this->assertSame($expected, $config); $backup = $this->loadConfig("dashboards/$user/dashboard.backup.ini"); $this->assertSame($initialConfig, $backup); } } public function testDashboardMigrationExpectsUserSwitch() { $this->expectException(MissingParameterException::class); $this->expectExceptionMessage('Required parameter \'user\' missing'); $command = $this->createCommandInstance(); $command->dashboardAction(); } /** * Checks the following: * - Whether only a single user is handled * - Whether shared items are migrated, depending on the owner * - Whether old configs are kept/or backups are created * - Whether a second run changes nothing, if nothing changed * - Whether a second run keeps the backup, if nothing changed * - Whether a new backup isn't created, if nothing changed */ public function testNavigationMigrationBehavesAsExpectedByDefault() { [$initialMenuConfig, $expectedMenu] = $this->getConfig('menu-items'); [$initialHostConfig, $expectedHosts] = $this->getConfig('host-actions'); [$initialServiceConfig, $expectedServices] = $this->getConfig('service-actions'); $this->createConfig('preferences/test/menu.ini', $initialMenuConfig); $this->createConfig('preferences/test/host-actions.ini', $initialHostConfig); $this->createConfig('preferences/test/service-actions.ini', $initialServiceConfig); $this->createConfig('preferences/test2/menu.ini', $initialMenuConfig); $this->createConfig('preferences/test2/host-actions.ini', $initialHostConfig); $this->createConfig('preferences/test2/service-actions.ini', $initialServiceConfig); [$initialSharedMenuConfig, $expectedSharedMenu] = $this->getConfig('shared-menu-items'); $this->createConfig('navigation/menu.ini', $initialSharedMenuConfig); [$initialSharedConfig, $expectedShared] = $this->getConfig('shared-host-actions'); $this->createConfig('navigation/host-actions.ini', $initialSharedConfig); $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); $menuConfig = $this->loadConfig('preferences/test/menu.ini'); $this->assertSame($expectedMenu, $menuConfig); $sharedMenuConfig = $this->loadConfig('navigation/menu.ini'); $this->assertSame($expectedSharedMenu, $sharedMenuConfig); $menuConfig2 = $this->loadConfig('preferences/test2/menu.ini'); $this->assertSame($initialMenuConfig, $menuConfig2); $menuBackup = $this->loadConfig('preferences/test/menu.backup.ini'); $this->assertSame($initialMenuConfig, $menuBackup); $hosts = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $services = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); $this->assertSame($expectedHosts, $hosts); $this->assertSame($expectedServices, $services); $sharedConfig = $this->loadConfig('navigation/icingadb-host-actions.ini'); $this->assertSame($expectedShared, $sharedConfig); $hosts2 = $this->loadConfig('preferences/test2/icingadb-host-actions.ini'); $services2 = $this->loadConfig('preferences/test2/icingadb-service-actions.ini'); $this->assertEmpty($hosts2); $this->assertEmpty($services2); $oldHosts = $this->loadConfig('preferences/test/host-actions.ini'); $oldServices = $this->loadConfig('preferences/test/service-actions.ini'); $this->assertSame($initialHostConfig, $oldHosts); $this->assertSame($initialServiceConfig, $oldServices); $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); $menuConfigAfterSecondRun = $this->loadConfig('preferences/test/menu.ini'); $this->assertSame($menuConfig, $menuConfigAfterSecondRun); $menuBackupAfterSecondRun = $this->loadConfig('preferences/test/menu.backup.ini'); $this->assertSame($menuBackup, $menuBackupAfterSecondRun); $menuBackup1AfterSecondRun = $this->loadConfig('preferences/test/menu.backup1.ini'); $this->assertEmpty($menuBackup1AfterSecondRun); $hostsAfterSecondRun = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $servicesAfterSecondRun = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); $this->assertSame($hosts, $hostsAfterSecondRun); $this->assertSame($services, $servicesAfterSecondRun); } /** * Checks the following: * - Whether a second run creates a new backup, if something changed * * @depends testNavigationMigrationBehavesAsExpectedByDefault */ public function testNavigationMigrationCreatesMultipleBackups() { $initialOldConfig = [ 'hosts' => [ 'title' => 'Host Problems', 'url' => 'monitoring/list/hosts?host_problem=1' ] ]; $initialNewConfig = [ 'hosts' => [ 'title' => 'Host Problems', 'url' => 'icingadb/hosts?host.state.is_problem=y' ], 'group_members' => [ 'title' => 'Group Members', 'url' => 'monitoring/list/hosts?hostgroup_name=group1|hostgroup_name=group2' ] ]; $expectedNewConfig = [ 'hosts' => [ 'title' => 'Host Problems', 'url' => 'icingadb/hosts?host.state.is_problem=y' ] ]; $expectedFinalConfig = [ 'hosts' => [ 'title' => 'Host Problems', 'url' => 'icingadb/hosts?host.state.is_problem=y' ], 'group_members' => [ 'title' => 'Group Members', 'url' => 'icingadb/hosts?hostgroup.name=group1|hostgroup.name=group2' ] ]; $this->createConfig('preferences/test/menu.ini', $initialOldConfig); $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); $newConfig = $this->loadConfig('preferences/test/menu.ini'); $this->assertSame($expectedNewConfig, $newConfig); $oldBackup = $this->loadConfig('preferences/test/menu.backup.ini'); $this->assertSame($initialOldConfig, $oldBackup); $this->createConfig('preferences/test/menu.ini', $initialNewConfig); $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); $finalConfig = $this->loadConfig('preferences/test/menu.ini'); $this->assertSame($expectedFinalConfig, $finalConfig); $newBackup = $this->loadConfig('preferences/test/menu.backup1.ini'); $this->assertSame($initialNewConfig, $newBackup); } /** * Checks the following: * - Whether backups are skipped * * @depends testNavigationMigrationBehavesAsExpectedByDefault */ public function testNavigationMigrationSkipsBackupIfRequested() { [$initialConfig, $expected] = $this->getConfig('menu-items'); $this->createConfig('preferences/test/menu.ini', $initialConfig); $command = $this->createCommandInstance('--user', 'test', '--no-backup'); $command->navigationAction(); $config = $this->loadConfig('preferences/test/menu.ini'); $this->assertSame($expected, $config); $backup = $this->loadConfig('preferences/test/menu.backup.ini'); $this->assertEmpty($backup); } /** * Checks the following: * - Whether existing Icinga DB Actions are transformed regarding wildcard filters */ public function testNavigationMigrationTransformsAlreadyExistingIcingaDBActions() { [$initialHostConfig, $expectedHosts] = $this->getConfig('icingadb-host-actions'); [$initialServiceConfig, $expectedServices] = $this->getConfig('icingadb-service-actions'); $this->createConfig('preferences/test/icingadb-host-actions.ini', $initialHostConfig); $this->createConfig('preferences/test/icingadb-service-actions.ini', $initialServiceConfig); $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); $hosts = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $services = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); $this->assertSame($expectedHosts, $hosts); $this->assertSame($expectedServices, $services); $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); $hostsAfterSecondRun = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $servicesAfterSecondRun = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); $this->assertSame($hosts, $hostsAfterSecondRun); $this->assertSame($services, $servicesAfterSecondRun); } /** * Checks the following: * - Whether legacy host/service macros are migrated */ public function testNavigationMigrationMigratesLegacyMacros() { [$initialHostConfig, $expectedHosts] = $this->getConfig('host-actions-legacy-macros'); [$initialServiceConfig, $expectedServices] = $this->getConfig('service-actions-legacy-macros'); $this->createConfig('preferences/test/host-actions.ini', $initialHostConfig); $this->createConfig('preferences/test/service-actions.ini', $initialServiceConfig); $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); $hosts = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $services = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); $this->assertSame($expectedHosts, $hosts); $this->assertSame($expectedServices, $services); } /** * Checks the following: * - Whether old configs are removed */ public function testNavigationMigrationDeletesOldConfigsIfRequested() { [$initialHostConfig, $expectedHosts] = $this->getConfig('host-actions'); [$initialServiceConfig, $expectedServices] = $this->getConfig('service-actions'); $this->createConfig('preferences/test/host-actions.ini', $initialHostConfig); $this->createConfig('preferences/test/service-actions.ini', $initialServiceConfig); $command = $this->createCommandInstance('--user', 'test', '--no-backup'); $command->navigationAction(); $hosts = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $services = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); $this->assertSame($expectedHosts, $hosts); $this->assertSame($expectedServices, $services); $oldHosts = $this->loadConfig('preferences/test/host-actions.ini'); $oldServices = $this->loadConfig('preferences/test/service-actions.ini'); $this->assertEmpty($oldHosts); $this->assertEmpty($oldServices); } /** * Checks the following: * - Whether existing configs are left alone by default * - Whether existing configs are overridden if requested */ public function testNavigationMigrationOverridesExistingActionsIfRequested() { $initialOldUserConfig = [ 'hosts' => [ 'type' => 'host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host_name=%2Afoo%2A' ] ]; $initialOldSharedConfig = [ 'hosts' => [ 'type' => 'host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host_name=%2Afoo%2A', 'owner' => 'test' ] ]; $initialNewUserConfig = [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name~%2Abar%2A' ] ]; $initialNewSharedConfig = [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name~%2Abar%2A', 'owner' => 'test' ] ]; $expectedFinalUserConfig = [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name~%2Afoo%2A' ] ]; $expectedFinalSharedConfig = [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name~%2Afoo%2A', 'owner' => 'test' ] ]; $this->createConfig('preferences/test/host-actions.ini', $initialOldUserConfig); $this->createConfig('preferences/test/icingadb-host-actions.ini', $initialNewUserConfig); $this->createConfig('navigation/host-actions.ini', $initialOldSharedConfig); $this->createConfig('navigation/icingadb-host-actions.ini', $initialNewSharedConfig); $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); $finalUserConfig = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $this->assertSame($initialNewUserConfig, $finalUserConfig); $finalSharedConfig = $this->loadConfig('navigation/icingadb-host-actions.ini'); $this->assertSame($initialNewSharedConfig, $finalSharedConfig); $command = $this->createCommandInstance('--user', 'test', '--override'); $command->navigationAction(); $finalUserConfig = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $this->assertSame($expectedFinalUserConfig, $finalUserConfig); $finalSharedConfig = $this->loadConfig('navigation/icingadb-host-actions.ini'); $this->assertSame($expectedFinalSharedConfig, $finalSharedConfig); } public function testNavigationMigrationExpectsUserSwitch() { $this->expectException(MissingParameterException::class); $this->expectExceptionMessage('Required parameter \'user\' missing'); $command = $this->createCommandInstance(); $command->navigationAction(); } /** * Checks the following: * - Whether only a single role is handled * - Whether role name matching works */ public function testRoleMigrationHandlesASingleRoleOnlyIfRequested() { [$initialConfig, $expected] = $this->getConfig('single-role-or-group'); $this->createConfig('roles.ini', $initialConfig); $command = $this->createCommandInstance('--role', 'one'); $command->roleAction(); $config = $this->loadConfig('roles.ini'); $this->assertSame($expected, $config); } /** * Checks the following: * - Whether only a single role is handled * - Whether group matching works */ public function testRoleMigrationHandlesARoleWithMatchingGroups() { [$initialConfig, $expected] = $this->getConfig('single-role-or-group'); $this->createConfig('roles.ini', $initialConfig); $command = $this->createCommandInstance('--group', 'support'); $command->roleAction(); $config = $this->loadConfig('roles.ini'); $this->assertSame($expected, $config); } /** * Checks the following: * - Whether permissions are properly migrated * - Whether refusals are properly migrated * - Whether restrictions are properly migrated * - Whether blacklists are properly migrated * - Whether backups are created * - Whether a second run changes nothing, if nothing changed * - Whether a second run keeps the backup, if nothing changed * - Whether a new backup isn't created, if nothing changed */ public function testRoleMigrationMigratesAllRoles() { [$initialConfig, $expected] = $this->getConfig('all-roles'); $this->createConfig('roles.ini', $initialConfig); $command = $this->createCommandInstance('--role', '*'); $command->roleAction(); $config = $this->loadConfig('roles.ini'); $this->assertSame($expected, $config); $backup = $this->loadConfig('roles.backup.ini'); $this->assertSame($initialConfig, $backup); $command = $this->createCommandInstance('--role', '*'); $command->roleAction(); $configAfterSecondRun = $this->loadConfig('roles.ini'); $this->assertSame($config, $configAfterSecondRun); $backupAfterSecondRun = $this->loadConfig('roles.backup.ini'); $this->assertSame($backup, $backupAfterSecondRun); $backup2 = $this->loadConfig('roles.backup1.ini'); $this->assertEmpty($backup2); } /** * Checks the following: * - Whether backups are skipped * * @depends testRoleMigrationMigratesAllRoles */ public function testRoleMigrationSkipsBackupIfRequested() { [$initialConfig, $expected] = $this->getConfig('all-roles'); $this->createConfig('roles.ini', $initialConfig); $command = $this->createCommandInstance('--role', '*', '--no-backup'); $command->roleAction(); $config = $this->loadConfig('roles.ini'); $this->assertSame($expected, $config); $backup = $this->loadConfig('roles.backup.ini'); $this->assertEmpty($backup); } /** * Checks the following: * - Whether monitoring's variable protection rules are migrated to all roles granting access to monitoring */ public function testRoleMigrationAlsoMigratesVariableProtections() { $initialConfig = [ 'one' => [ 'permissions' => 'module/monitoring' ], 'two' => [ 'permissions' => 'module/monitoring' ], 'three' => [ 'permissions' => 'module/reporting' ] ]; $expectedConfig = [ 'one' => [ 'permissions' => 'module/monitoring', 'icingadb/protect/variables' => 'ob.*,env' ], 'two' => [ 'permissions' => 'module/monitoring', 'icingadb/protect/variables' => 'ob.*,env' ], 'three' => [ 'permissions' => 'module/reporting' ] ]; $this->createConfig('modules/monitoring/config.ini', [ 'security' => [ 'protected_customvars' => 'ob.*,env' ] ]); // Invalidate config cache Config::module('monitoring', 'config', true); $this->createConfig('roles.ini', $initialConfig); $command = $this->createCommandInstance('--role', '*'); $command->roleAction(); $config = $this->loadConfig('roles.ini'); $this->assertSame($expectedConfig, $config); } /** * Checks the following: * - Whether already migrated roles are skipped during migration * - Whether already migrated roles are transformed regarding wildcard filters */ public function testRoleMigrationSkipsRolesThatAlreadyGrantAccessToIcingaDbButTransformWildcardRestrictions() { $initialConfig = [ 'only-monitoring' => [ 'permissions' => 'module/monitoring,monitoring/command/comment/*', 'monitoring/filter/objects' => 'host_name=%2Afoo%2A' ], 'monitoring-and-icingadb' => [ 'permissions' => 'module/monitoring,monitoring/command/comment/*,module/icingadb', 'monitoring/filter/objects' => 'host_name=%2Abar%2A', 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' ] ]; $expectedConfig = [ 'only-monitoring' => [ 'permissions' => 'module/monitoring,monitoring/command/comment/*,icingadb/command/comment/*', 'monitoring/filter/objects' => 'host_name=%2Afoo%2A', 'icingadb/filter/objects' => 'host.name~%2Afoo%2A' ], 'monitoring-and-icingadb' => [ 'permissions' => 'module/monitoring,monitoring/command/comment/*,module/icingadb', 'monitoring/filter/objects' => 'host_name=%2Abar%2A', 'icingadb/filter/objects' => 'host.name~%2Afoo%2A' ] ]; $this->createConfig('roles.ini', $initialConfig); $command = $this->createCommandInstance('--role', '*'); $command->roleAction(); $config = $this->loadConfig('roles.ini'); $this->assertSame($expectedConfig, $config); } /** * Checks the following: * - Whether already migrated roles are reset if requested */ public function testRoleMigrationOverridesAlreadyMigratedRolesIfRequested() { $initialConfig = [ 'only-monitoring' => [ 'permissions' => 'module/monitoring,monitoring/command/comment/*', 'monitoring/filter/objects' => 'host_name=%2Afoo%2A' ], 'monitoring-and-icingadb' => [ 'permissions' => 'module/monitoring,monitoring/command/comment/*,module/icingadb', 'monitoring/filter/objects' => 'host_name=%2Abar%2A', 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' ] ]; $expectedConfig = [ 'only-monitoring' => [ 'permissions' => 'module/monitoring,monitoring/command/comment/*,icingadb/command/comment/*', 'monitoring/filter/objects' => 'host_name=%2Afoo%2A', 'icingadb/filter/objects' => 'host.name~%2Afoo%2A' ], 'monitoring-and-icingadb' => [ 'permissions' => 'module/monitoring' . ',monitoring/command/comment/*' . ',icingadb/command/comment/*', 'monitoring/filter/objects' => 'host_name=%2Abar%2A', 'icingadb/filter/objects' => 'host.name~%2Abar%2A' ] ]; $this->createConfig('roles.ini', $initialConfig); $command = $this->createCommandInstance('--role', '*', '--override'); $command->roleAction(); $config = $this->loadConfig('roles.ini'); $this->assertSame($expectedConfig, $config); } public function testRoleMigrationExpectsTheRoleOrGroupSwitch() { $this->expectException(IcingaException::class); $this->expectExceptionMessage("One of the parameters 'group' or 'role' must be supplied"); $command = $this->createCommandInstance(); $command->roleAction(); } public function testRoleMigrationExpectsEitherTheRoleOrGroupSwitchButNotBoth() { $this->expectException(IcingaException::class); $this->expectExceptionMessage("Use either 'group' or 'role'. Both cannot be used as role overrules group."); $command = $this->createCommandInstance('--role=foo', '--group=bar'); $command->roleAction(); } public function testFilterMigrationWorksAsExpected() { $initialHostActionConfig = [ 'hosts' => [ 'type' => 'host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host_name=%2Afoo%2A' ] ]; $expectedHostActionConfig = $initialHostActionConfig; $initialIcingadbHostActionConfig = [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name=%2Afoo%2A' ] ]; $expectedIcingadbHostActionConfig = [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name~%2Afoo%2A' ] ]; $initialServiceActionConfig = [ 'services' => [ 'type' => 'service-action', 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', 'filter' => '_service_foo=bar&_service_bar=%2Afoo%2A' ] ]; $expectedServiceActionConfig = $initialServiceActionConfig; $initialIcingadbServiceActionConfig = [ 'services' => [ 'type' => 'icingadb-service-action', 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', 'filter' => 'service.vars.foo=bar&service.vars.bar=%2Afoo%2A' ] ]; $expectedIcingadbServiceActionConfig = [ 'services' => [ 'type' => 'icingadb-service-action', 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', 'filter' => 'service.vars.foo=bar&service.vars.bar~%2Afoo%2A' ] ]; $initialMenuConfig = [ 'foreign-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'example.com?q=foo' ], 'monitoring-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'monitoring/list/hosts?host_problem=1' ], 'icingadb-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'icingadb/hosts?host.name=%2Afoo%2A' ] ]; $expectedMenuConfig = [ 'foreign-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'example.com?q=foo' ], 'monitoring-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'monitoring/list/hosts?host_problem=1' ], 'icingadb-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'icingadb/hosts?host.name~%2Afoo%2A' ] ]; $initialDashboardConfig = [ 'hosts' => [ 'title' => 'Hosts' ], 'hosts.problems' => [ 'title' => 'Host Problems', 'url' => 'monitoring/list/hosts?host_problem=1' ], 'icingadb' => [ 'title' => 'Icinga DB' ], 'icingadb.wildcards' => [ 'title' => 'Wildcards', 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name=%2Alinux%2A' ] ]; $expectedDashboardConfig = [ 'hosts' => [ 'title' => 'Hosts' ], 'hosts.problems' => [ 'title' => 'Host Problems', 'url' => 'monitoring/list/hosts?host_problem=1' ], 'icingadb' => [ 'title' => 'Icinga DB' ], 'icingadb.wildcards' => [ 'title' => 'Wildcards', 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name~%2Alinux%2A' ] ]; $initialRoleConfig = [ 'one' => [ 'groups' => 'support,helpdesk', 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'two' => [ 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'three' => [ 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' ] ]; $expectedRoleConfig = [ 'one' => [ 'groups' => 'support,helpdesk', 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'two' => [ 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'three' => [ 'icingadb/filter/objects' => 'host.name~%2Afoo%2A' ] ]; $this->createConfig('preferences/test/host-actions.ini', $initialHostActionConfig); $this->createConfig('preferences/test/service-actions.ini', $initialServiceActionConfig); $this->createConfig('preferences/test/icingadb-host-actions.ini', $initialIcingadbHostActionConfig); $this->createConfig('preferences/test/icingadb-service-actions.ini', $initialIcingadbServiceActionConfig); $this->createConfig('dashboards/test/dashboard.ini', $initialDashboardConfig); $this->createConfig('preferences/test/menu.ini', $initialMenuConfig); $this->createConfig('roles.ini', $initialRoleConfig); $command = $this->createCommandInstance(); $command->filterAction(); $hostActionConfig = $this->loadConfig('preferences/test/host-actions.ini'); $serviceActionConfig = $this->loadConfig('preferences/test/service-actions.ini'); $icingadbHostActionConfig = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $icingadbServiceActionConfig = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); $dashboardBackup = $this->loadConfig('dashboards/test/dashboard.backup.ini'); $dashboardConfig = $this->loadConfig('dashboards/test/dashboard.ini'); $menuBackup = $this->loadConfig('preferences/test/menu.backup.ini'); $menuConfig = $this->loadConfig('preferences/test/menu.ini'); $roleBackup = $this->loadConfig('roles.backup.ini'); $roleConfig = $this->loadConfig('roles.ini'); $this->assertSame($expectedHostActionConfig, $hostActionConfig); $this->assertSame($expectedServiceActionConfig, $serviceActionConfig); $this->assertSame($initialDashboardConfig, $dashboardBackup); $this->assertSame($initialMenuConfig, $menuBackup); $this->assertSame($initialRoleConfig, $roleBackup); $this->assertSame($expectedIcingadbHostActionConfig, $icingadbHostActionConfig); $this->assertSame($expectedIcingadbServiceActionConfig, $icingadbServiceActionConfig); $this->assertSame($expectedDashboardConfig, $dashboardConfig); $this->assertSame($expectedMenuConfig, $menuConfig); $this->assertSame($expectedRoleConfig, $roleConfig); } /** * @depends testFilterMigrationWorksAsExpected */ public function testFilterMigrationSkipsBackupsIfRequested() { $initialHostActionConfig = [ 'hosts' => [ 'type' => 'host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host_name=%2Afoo%2A' ] ]; $expectedHostActionConfig = $initialHostActionConfig; $initialIcingadbHostActionConfig = [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name=%2Afoo%2A' ] ]; $expectedIcingadbHostActionConfig = [ 'hosts' => [ 'type' => 'icingadb-host-action', 'url' => 'example.com/search?q=$host.name$', 'filter' => 'host.name~%2Afoo%2A' ] ]; $initialServiceActionConfig = [ 'services' => [ 'type' => 'service-action', 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', 'filter' => '_service_foo=bar&_service_bar=%2Afoo%2A' ] ]; $expectedServiceActionConfig = $initialServiceActionConfig; $initialIcingadbServiceActionConfig = [ 'services' => [ 'type' => 'icingadb-service-action', 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', 'filter' => 'service.vars.foo=bar&service.vars.bar=%2Afoo%2A' ] ]; $expectedIcingadbServiceActionConfig = [ 'services' => [ 'type' => 'icingadb-service-action', 'url' => 'example.com/search?q=$service.name$,$host.name$,$host.address$,$host.address6$', 'filter' => 'service.vars.foo=bar&service.vars.bar~%2Afoo%2A' ] ]; $initialMenuConfig = [ 'foreign-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'example.com?q=foo' ], 'monitoring-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'monitoring/list/hosts?host_problem=1' ], 'icingadb-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'icingadb/hosts?host.name=%2Afoo%2A' ] ]; $expectedMenuConfig = [ 'foreign-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'example.com?q=foo' ], 'monitoring-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'monitoring/list/hosts?host_problem=1' ], 'icingadb-url' => [ 'type' => 'menu-item', 'target' => '_blank', 'url' => 'icingadb/hosts?host.name~%2Afoo%2A' ] ]; $initialDashboardConfig = [ 'hosts' => [ 'title' => 'Hosts' ], 'hosts.problems' => [ 'title' => 'Host Problems', 'url' => 'monitoring/list/hosts?host_problem=1' ], 'icingadb' => [ 'title' => 'Icinga DB' ], 'icingadb.wildcards' => [ 'title' => 'Wildcards', 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name=%2Alinux%2A' ] ]; $expectedDashboardConfig = [ 'hosts' => [ 'title' => 'Hosts' ], 'hosts.problems' => [ 'title' => 'Host Problems', 'url' => 'monitoring/list/hosts?host_problem=1' ], 'icingadb' => [ 'title' => 'Icinga DB' ], 'icingadb.wildcards' => [ 'title' => 'Wildcards', 'url' => 'icingadb/hosts?host.state.is_problem=y&hostgroup.name~%2Alinux%2A' ] ]; $initialRoleConfig = [ 'one' => [ 'groups' => 'support,helpdesk', 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'two' => [ 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'three' => [ 'icingadb/filter/objects' => 'host.name=%2Afoo%2A' ] ]; $expectedRoleConfig = [ 'one' => [ 'groups' => 'support,helpdesk', 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'two' => [ 'monitoring/filter/objects' => 'host_name=foo|hostgroup_name=foo' ], 'three' => [ 'icingadb/filter/objects' => 'host.name~%2Afoo%2A' ] ]; $this->createConfig('preferences/test/host-actions.ini', $initialHostActionConfig); $this->createConfig('preferences/test/service-actions.ini', $initialServiceActionConfig); $this->createConfig('preferences/test/icingadb-host-actions.ini', $initialIcingadbHostActionConfig); $this->createConfig('preferences/test/icingadb-service-actions.ini', $initialIcingadbServiceActionConfig); $this->createConfig('dashboards/test/dashboard.ini', $initialDashboardConfig); $this->createConfig('preferences/test/menu.ini', $initialMenuConfig); $this->createConfig('roles.ini', $initialRoleConfig); $command = $this->createCommandInstance('--no-backup'); $command->filterAction(); $hostActionConfig = $this->loadConfig('preferences/test/host-actions.ini'); $serviceActionConfig = $this->loadConfig('preferences/test/service-actions.ini'); $icingadbHostActionConfig = $this->loadConfig('preferences/test/icingadb-host-actions.ini'); $icingadbServiceActionConfig = $this->loadConfig('preferences/test/icingadb-service-actions.ini'); $dashboardBackup = $this->loadConfig('dashboards/test/dashboard.backup.ini'); $dashboardConfig = $this->loadConfig('dashboards/test/dashboard.ini'); $menuBackup = $this->loadConfig('preferences/test/menu.backup.ini'); $menuConfig = $this->loadConfig('preferences/test/menu.ini'); $roleBackup = $this->loadConfig('roles.backup.ini'); $roleConfig = $this->loadConfig('roles.ini'); $this->assertSame($expectedHostActionConfig, $hostActionConfig); $this->assertSame($expectedServiceActionConfig, $serviceActionConfig); $this->assertEmpty($dashboardBackup); $this->assertEmpty($menuBackup); $this->assertEmpty($roleBackup); $this->assertSame($expectedIcingadbHostActionConfig, $icingadbHostActionConfig); $this->assertSame($expectedIcingadbServiceActionConfig, $icingadbServiceActionConfig); $this->assertSame($expectedDashboardConfig, $dashboardConfig); $this->assertSame($expectedMenuConfig, $menuConfig); $this->assertSame($expectedRoleConfig, $roleConfig); } public function testNavigationMigrationWorksEvenIfOnlySharedItemsExist() { $this->expectNotToPerformAssertions(); $this->createConfig('navigation/menu.ini', []); $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); } public function testNavigationMigrationWorksEvenIfOnlyUserItemsExist() { $this->expectNotToPerformAssertions(); $this->createConfig('preferences/test/menu.ini', []); $command = $this->createCommandInstance('--user', 'test'); $command->navigationAction(); } public function testDashboardMigrationWorksEvenIfNoDashboardsExist() { $this->expectNotToPerformAssertions(); $command = $this->createCommandInstance('--user', 'test'); $command->dashboardAction(); } } icingadb-web-1.4.0/test/php/library/000077500000000000000000000000001516124654200172475ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/library/Icingadb/000077500000000000000000000000001516124654200207475ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/library/Icingadb/Command/000077500000000000000000000000001516124654200223255ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/library/Icingadb/Command/Transport/000077500000000000000000000000001516124654200243215ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/library/Icingadb/Command/Transport/CommandTransportTest.php000066400000000000000000000230641516124654200311720ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Module\Icingadb\Command\Transport; use Icinga\Module\Icingadb\Command\Object\AddCommentCommand; use Icinga\Module\Icingadb\Command\Transport\CommandTransportException; use Icinga\Module\Icingadb\Model\Host; use PHPUnit\Framework\TestCase; use Tests\Icinga\Module\Icingadb\Lib\FailoverCommandTransport; use Tests\Icinga\Module\Icingadb\Lib\IntermittentlyFailingCommandTransport; use Tests\Icinga\Module\Icingadb\Lib\StrikingCommandTransport; class CommandTransportTest extends TestCase { public function testFatalErrorHandling(): void { $this->expectException(CommandTransportException::class); $this->expectExceptionMessage('endpointA strikes!'); (new StrikingCommandTransport())->send( (new AddCommentCommand()) ->setExpireTime(42) ->setAuthor('GLaDOS') ->setComment('The cake is a lie') ->setObjects(new \CallbackFilterIterator(new \ArrayIterator([ (new Host())->setProperties(['name' => 'host1']), (new Host())->setProperties(['name' => 'host2']), ]), function ($host) { return $host->name === 'host2'; })) ); } public function testFallbackHandling(): void { $result = (new FailoverCommandTransport())->send( (new AddCommentCommand()) ->setExpireTime(42) ->setAuthor('GLaDOS') ->setComment('The cake is a lie') ->setObjects(new \ArrayIterator([ (new Host())->setProperties(['name' => 'host1']), (new Host())->setProperties(['name' => 'host2']), ])) ); $this->assertSame( [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host1', 'host2'] ], $result ); } public function testGeneratorsAreNotSupported(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Generators are not supported'); (new StrikingCommandTransport())->send( (new AddCommentCommand()) ->setExpireTime(42) ->setAuthor('GLaDOS') ->setComment('The cake is a lie') ->setObjects((function () { yield (new Host())->setProperties(['name' => 'host1']); yield (new Host())->setProperties(['name' => 'host2']); })()) ); } public function testChunkedObjectsWithFallbackHandling(): void { // Multiple chunks $result = (new FailoverCommandTransport())->send( (new AddCommentCommand()) ->setExpireTime(42) ->setAuthor('GLaDOS') ->setComment('The cake is a lie') ->setChunkSize(1) ->setObjects(new \ArrayIterator([ (new Host())->setProperties(['name' => 'host1']), (new Host())->setProperties(['name' => 'host2']), ])) ); $this->assertSame( [ [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host1'] ], [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host2'] ] ], $result ); // A single chunk $result = (new FailoverCommandTransport())->send( (new AddCommentCommand()) ->setExpireTime(42) ->setAuthor('GLaDOS') ->setComment('The cake is a lie') ->setChunkSize(4) ->setObjects(new \ArrayIterator([ (new Host())->setProperties(['name' => 'host1']), (new Host())->setProperties(['name' => 'host2']), ])) ); $this->assertSame( [ [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host1', 'host2'] ] ], $result ); } public function testIntermittentFailureHandlingDuringChunkedTransmission(): void { // Fails after the 2nd chunk $result = (new IntermittentlyFailingCommandTransport())->send( (new AddCommentCommand()) ->setExpireTime(42) ->setAuthor('GLaDOS') ->setComment('The cake is a lie') ->setChunkSize(1) ->setObjects(new \CallbackFilterIterator(new \ArrayIterator([ (new Host())->setProperties(['name' => 'host1']), (new Host())->setProperties(['name' => 'host2']), (new Host())->setProperties(['name' => 'host3']), (new Host())->setProperties(['name' => 'host4']), (new Host())->setProperties(['name' => 'host5']), (new Host())->setProperties(['name' => 'host6']), ]), fn() => true)) ); $this->assertSame( [ [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host1'], 'endpoint' => 'endpointA' ], [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host2'], 'endpoint' => 'endpointB' ], [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host3'], 'endpoint' => 'endpointB' ], [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host4'], 'endpoint' => 'endpointB' ], [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host5'], 'endpoint' => 'endpointB' ], [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host6'], 'endpoint' => 'endpointB' ] ], $result ); // Fails after the next-to-last chunk IntermittentlyFailingCommandTransport::$failAtAttemptNo = 3; IntermittentlyFailingCommandTransport::$attemptNo = 0; $result = (new IntermittentlyFailingCommandTransport())->send( (new AddCommentCommand()) ->setExpireTime(42) ->setAuthor('GLaDOS') ->setComment('The cake is a lie') ->setChunkSize(2) ->setObjects(new \CallbackFilterIterator(new \ArrayIterator([ (new Host())->setProperties(['name' => 'host1']), (new Host())->setProperties(['name' => 'host2']), (new Host())->setProperties(['name' => 'host3']), (new Host())->setProperties(['name' => 'host4']), (new Host())->setProperties(['name' => 'host5']), (new Host())->setProperties(['name' => 'host6']), ]), fn() => true)) ); $this->assertSame( [ [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host1', 'host2'], 'endpoint' => 'endpointA' ], [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host3', 'host4'], 'endpoint' => 'endpointA' ], [ 'author' => 'GLaDOS', 'comment' => 'The cake is a lie', 'expiry' => 42, 'hosts' => ['host5', 'host6'], 'endpoint' => 'endpointB' ] ], $result ); } public function testFatalErrorHandlingDuringChunkedTransmission(): void { $this->expectException(CommandTransportException::class); $this->expectExceptionMessage('endpointA strikes!'); (new StrikingCommandTransport())->send( (new AddCommentCommand()) ->setExpireTime(42) ->setAuthor('GLaDOS') ->setComment('The cake is a lie') ->setChunkSize(1) ->setObjects(new \ArrayIterator([ (new Host())->setProperties(['name' => 'host1']), (new Host())->setProperties(['name' => 'host2']), ])) ); } } icingadb-web-1.4.0/test/php/library/Icingadb/Common/000077500000000000000000000000001516124654200221775ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/library/Icingadb/Common/AuthTest.php000066400000000000000000000122151516124654200244520ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Modules\Icingadb\Common; use ipl\Stdlib\Filter; use ipl\Web\Filter\Renderer; use PHPUnit\Framework\TestCase; use Icinga\Module\Icingadb\Common\Auth; class AuthTest extends TestCase { use Auth; public function testFlatteningOfNestedAnyRules(): void { $to = Filter::any(); $from = Filter::any( Filter::equal('a', 1), Filter::equal('b', 2), Filter::any( Filter::equal('c', 3), Filter::equal('d', 4), Filter::any( Filter::equal('e', 5), Filter::equal('f', 6) ) ) ); $this->flattenSemanticallyEqualRules($to, $from); $this->assertFilterEquals( '(a=1|b=2|c=3|d=4|e=5|f=6)', $to ); } public function testFlatteningOfNestedAllRules(): void { $to = Filter::all(); $from = Filter::all( Filter::equal('a', 1), Filter::equal('b', 2), Filter::all( Filter::equal('c', 3), Filter::equal('d', 4), Filter::all( Filter::equal('e', 5), Filter::equal('f', 6) ) ) ); $this->flattenSemanticallyEqualRules($to, $from); $this->assertFilterEquals( '(a=1&b=2&c=3&d=4&e=5&f=6)', $to ); } public function testFlatteningOfNestedNoneRules(): void { $to = Filter::none(); $from = Filter::none( Filter::equal('a', 1), Filter::equal('b', 2), Filter::none( Filter::equal('c', 3), Filter::equal('d', 4), Filter::none( Filter::equal('e', 5), Filter::equal('f', 6) ) ) ); $this->flattenSemanticallyEqualRules($to, $from); $this->assertFilterEquals( '!(a=1|b=2|c=3|d=4|e=5|f=6)', $to ); } public function testFlatteningOfNestedMixedRules(): void { $to = Filter::any(); $from = Filter::any( Filter::equal('a', 1), Filter::all( Filter::equal('b', 2), Filter::equal('c', 3) ), Filter::none( Filter::equal('d', 4), Filter::equal('e', 5) ), Filter::any( Filter::equal('f', 6), Filter::equal('g', 7), Filter::none( Filter::none( Filter::equal('h', 8), Filter::equal('i', 9) ) ) ) ); $this->flattenSemanticallyEqualRules($to, $from); $this->assertFilterEquals( '(a=1|(b=2&c=3)|!(d=4|e=5)|f=6|g=7|!(h=8|i=9))', $to ); } public function testFlatteningOfEdgeCases(): void { $to = Filter::any(); $from = Filter::any( Filter::all( Filter::equal('a', 1) ), Filter::none( Filter::equal('b', 2) ), Filter::any( Filter::equal('c', 3) ), Filter::equal('d', 4) ); $this->flattenSemanticallyEqualRules($to, $from); $this->assertFilterEquals( '(a=1|!(b=2)|c=3|d=4)', $to ); } public function testFlatteningOfSemanticallyUnequalRules(): void { $to = Filter::any(); $from = Filter::all( Filter::equal('a', 1), Filter::equal('b', 2), Filter::any( Filter::equal('c', 3), Filter::equal('d', 4), Filter::any( Filter::equal('e', 5), Filter::equal('f', 6) ) ), Filter::all( Filter::equal('g', 7), ) ); $this->flattenSemanticallyEqualRules($to, $from); $this->assertFilterEquals( '((a=1&b=2&(c=3|d=4|e=5|f=6)&g=7)|)', $to ); $to = Filter::all(); $from = Filter::any( Filter::equal('a', 1), Filter::all( Filter::equal('b', 2), Filter::equal('c', 3) ), Filter::none( Filter::equal('d', 4), Filter::equal('e', 5) ), Filter::any( Filter::equal('f', 6), Filter::equal('g', 7) ) ); $this->flattenSemanticallyEqualRules($to, $from); $this->assertFilterEquals( '((a=1|(b=2&c=3)|!(d=4|e=5)|f=6|g=7))', $to ); } private function assertFilterEquals(string $expected, Filter\Rule $actual): void { $this->assertEquals( $expected, (new Renderer($actual))->setStrict()->render() ); } } icingadb-web-1.4.0/test/php/library/Icingadb/Common/MacrosTest.php000066400000000000000000000147621516124654200250060ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Modules\Icingadb\Common; use Icinga\Module\Icingadb\Common\Macros; use Icinga\Module\Icingadb\Compat\CompatHost; use Icinga\Module\Icingadb\Compat\CompatService; use Icinga\Module\Icingadb\Model\Host; use Icinga\Module\Icingadb\Model\Service; use ipl\Orm\Query; use ipl\Orm\ResultSet; use PHPUnit\Framework\TestCase; class MacrosTest extends TestCase { use Macros; public const VARS = [ 'os' => "Ubuntu", 'days[0]' => 'mo', 'days[1]' => 'tue', 'days[2]' => 'wed', 'days[3]' => 'thu', 'days[4]' => 'fr' ]; public function testHostMacros() { $host = new Host(); $host->name = 'test'; $host->address = '1.1.1.1'; $host->address6 = '::1'; $host->vars = self::VARS; $host->hostgroup = new Query(); $this->performHostMacroTests($host, $host); } public function testHostMacrosOnCompatObject() { if (! class_exists('Icinga\Module\Monitoring\Object\Host')) { $this->markTestSkipped('This test requires the monitoring module'); } $host = new Host(); $host->name = 'test'; $host->address = '1.1.1.1'; $host->address6 = '::1'; $host->vars = self::VARS; $host->hostgroup = new Query(); $compatHost = new CompatHost($host); $this->performHostMacroTests($compatHost, $host); } protected function performHostMacroTests($host, $source) { $this->assertEquals($source->name, $this->expandMacros('$host.name$', $host)); $this->assertEquals($source->name, $this->expandMacros('$name$', $host)); $this->assertEquals($source->address, $this->expandMacros('$host.address$', $host)); $this->assertEquals($source->address6, $this->expandMacros('$host.address6$', $host)); // A Host can have more than one hostgroups $this->assertEquals('$host.hostgroup$', $this->expandMacros('$host.hostgroup$', $host)); $this->assertEquals('$host.hostgroup.name$', $this->expandMacros('$host.hostgroup.name$', $host)); // Host custom vars $this->assertEquals($source->vars['os'], $this->expandMacros('$host.vars.os$', $host)); $this->assertEquals($source->vars['os'], $this->expandMacros('$vars.os$', $host)); $this->assertEquals($source->vars['days[2]'], $this->expandMacros('$vars.days[2]$', $host)); $this->assertEquals($source->vars['days[4]'], $this->expandMacros('$host.vars.days[4]$', $host)); // Host to service relation $this->assertEquals('$service.name$', $this->expandMacros('$service.name$', $host)); $this->assertEquals('$service.address$', $this->expandMacros('$service.address$', $host)); // Service custom vars $this->assertEquals('$service.vars.os$', $this->expandMacros('$service.vars.os$', $host)); $this->assertEquals('$service.vars.days[0]$', $this->expandMacros('$service.vars.days[0]$', $host)); $this->assertEquals('$service.vars.days[2]$', $this->expandMacros('$service.vars.days[2]$', $host)); } public function testServiceMacros() { $service = new Service(); $service->name = 'test-service'; $service->description = 'A test service'; $service->vars = self::VARS; $service->servicegroup = new Query(); $host = new Host(); $host->name = 'test'; $host->address = '1.1.1.1'; $host->hostgroup = new ResultSet(new \ArrayIterator()); $host->vars = self::VARS; $service->host = $host; $this->performServiceMacroTests($service, $service); } public function testServiceMacrosOnCompatObject() { if (! class_exists('Icinga\Module\Monitoring\Object\Service')) { $this->markTestSkipped('This test requires the monitoring module'); } $service = new Service(); $service->name = 'test-service'; $service->description = 'A test service'; $service->vars = self::VARS; $service->servicegroup = new Query(); $host = new Host(); $host->name = 'test'; $host->address = '1.1.1.1'; $host->hostgroup = new ResultSet(new \ArrayIterator()); $host->vars = self::VARS; $service->host = $host; $compatService = new CompatService($service); $this->performServiceMacroTests($compatService, $service); } protected function performServiceMacroTests($service, $source) { $this->assertEquals($source->name, $this->expandMacros('$service.name$', $service)); $this->assertEquals($source->name, $this->expandMacros('$name$', $service)); $this->assertEquals($source->description, $this->expandMacros('$service.description$', $service)); // A Service can have more than one hostgroups $this->assertEquals( '$service.servicegroup$', $this->expandMacros('$service.servicegroup$', $service) ); $this->assertEquals( '$service.servicegroup.name$', $this->expandMacros('$service.servicegroup.name$', $service) ); // Service custom vars $this->assertEquals($source->vars['os'], $this->expandMacros('$service.vars.os$', $service)); $this->assertEquals($source->vars['os'], $this->expandMacros('$vars.os$', $service)); $this->assertEquals($source->vars['days[2]'], $this->expandMacros('$vars.days[2]$', $service)); $this->assertEquals($source->vars['days[4]'], $this->expandMacros('$service.vars.days[4]$', $service)); $this->assertEquals($source->host->name, $this->expandMacros('$host.name$', $service)); $this->assertEquals($source->host->address, $this->expandMacros('$host.address$', $service)); // Host custom vars $this->assertEquals($source->host->vars['os'], $this->expandMacros('$host.vars.os$', $service)); $this->assertEquals($source->host->vars['days[0]'], $this->expandMacros('$host.vars.days[0]$', $service)); $this->assertEquals($source->host->vars['days[3]'], $this->expandMacros('$host.vars.days[3]$', $service)); // A Host can have more than one hostgroups $this->assertEquals( '$host.hostgroup$', $this->expandMacros('$host.hostgroup$', $service) ); $this->assertEquals( '$host.hostgroup.name$', $this->expandMacros('$host.hostgroup.name$', $service) ); } } icingadb-web-1.4.0/test/php/library/Icingadb/Common/StateBadgesTest.php000066400000000000000000000047271516124654200257500ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Modules\Icingadb\Common; use Icinga\Module\Icingadb\Common\StateBadges; use Icinga\Web\UrlParams; use ipl\Stdlib\Filter; use ipl\Web\Filter\QueryString; use ipl\Web\Url; use ipl\Web\Widget\Link; use PHPUnit\Framework\TestCase; class StateBadgesTest extends TestCase { public function testCreateLinkRendersBaseFilterCorrectly() { $stateBadges = $this->createStateBadges() ->setBaseFilter(Filter::any( Filter::equal('foo', 'bar'), Filter::equal('bar', 'foo') )); $link = $stateBadges->generateLink('test', Filter::equal('rab', 'oof')); $this->assertSame( 'rab=oof&(foo=bar|bar=foo)', $link->getUrl()->getQueryString() ); } private function createStateBadges() { $queryString = null; $urlMock = $this->createConfiguredMock(Url::class, [ 'getBasePath' => 'test', 'getParams' => $this->createConfiguredMock(UrlParams::class, [ 'toArray' => [] ]) ]); $urlMock->method('setFilter')->willReturnCallback( function ($qs) use ($urlMock, &$queryString) { $queryString = QueryString::render($qs); return $urlMock; } ); $urlMock->method('getQueryString')->willReturnCallback( function () use (&$queryString) { return $queryString; } ); return new class ($urlMock) extends StateBadges { private $urlMock; public function __construct($urlMock) { $this->urlMock = $urlMock; parent::__construct((object) []); } protected function getBaseUrl(): Url { return $this->urlMock; } protected function getType(): string { return 'test'; } protected function getPrefix(): string { return 'Test'; } protected function getStateInt(string $state): int { return 0; } public function generateLink($content, ?Filter\Rule $filter = null): Link { return parent::createLink($content, $filter); } }; } } icingadb-web-1.4.0/test/php/library/Icingadb/Model/000077500000000000000000000000001516124654200220075ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/library/Icingadb/Model/Behavior/000077500000000000000000000000001516124654200235465ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/library/Icingadb/Model/Behavior/FlattenedObjectVarsTest.php000066400000000000000000000166031516124654200310160ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Modules\Icingadb\Model\Behavior; use Icinga\Module\Icingadb\Common\Backend; use Icinga\Module\Icingadb\Model\Host; use ipl\Sql\Connection; use ipl\Sql\Test\SqlAssertions; use ipl\Sql\Test\TestConnection; use ipl\Stdlib\Filter; use PHPUnit\Framework\TestCase; class FlattenedObjectVarsTest extends TestCase { use SqlAssertions; private const SINGLE_UNEQUAL_RESULT = <<<'SQL' SELECT host.id FROM host WHERE (host.id NOT IN ((SELECT sub_customvar_flat_host.id AS sub_customvar_flat_host_id FROM customvar_flat sub_customvar_flat INNER JOIN host_customvar sub_customvar_flat_t_host_customvar ON sub_customvar_flat_t_host_customvar.customvar_id = sub_customvar_flat.customvar_id INNER JOIN host sub_customvar_flat_host ON sub_customvar_flat_host.id = sub_customvar_flat_t_host_customvar.host_id WHERE ((sub_customvar_flat.flatname = ?) AND (sub_customvar_flat.flatvalue = ?)) AND (sub_customvar_flat_host.id IS NOT NULL) GROUP BY sub_customvar_flat_host.id HAVING COUNT(DISTINCT sub_customvar_flat.id) >= ?)) OR host.id IS NULL) ORDER BY host.id SQL; private const DOUBLE_UNEQUAL_RESULT = <<<'SQL' SELECT host.id FROM host WHERE (host.id NOT IN ((SELECT sub_customvar_flat_host.id AS sub_customvar_flat_host_id FROM customvar_flat sub_customvar_flat INNER JOIN host_customvar sub_customvar_flat_t_host_customvar ON sub_customvar_flat_t_host_customvar.customvar_id = sub_customvar_flat.customvar_id INNER JOIN host sub_customvar_flat_host ON sub_customvar_flat_host.id = sub_customvar_flat_t_host_customvar.host_id WHERE (((sub_customvar_flat.flatname = ?) AND (sub_customvar_flat.flatvalue = ?)) OR ((sub_customvar_flat.flatname = ?) AND (sub_customvar_flat.flatvalue = ?))) AND (sub_customvar_flat_host.id IS NOT NULL) GROUP BY sub_customvar_flat_host.id HAVING COUNT(DISTINCT sub_customvar_flat.id) >= ?)) OR host.id IS NULL) ORDER BY host.id SQL; private const EQUAL_UNEQUAL_RESULT = <<<'SQL' SELECT host.id FROM host WHERE ((host.id NOT IN ((SELECT sub_customvar_flat_host.id AS sub_customvar_flat_host_id FROM customvar_flat sub_customvar_flat INNER JOIN host_customvar sub_customvar_flat_t_host_customvar ON sub_customvar_flat_t_host_customvar.customvar_id = sub_customvar_flat.customvar_id INNER JOIN host sub_customvar_flat_host ON sub_customvar_flat_host.id = sub_customvar_flat_t_host_customvar.host_id WHERE ((sub_customvar_flat.flatname = ?) AND (sub_customvar_flat.flatvalue = ?)) AND (sub_customvar_flat_host.id IS NOT NULL) GROUP BY sub_customvar_flat_host.id HAVING COUNT(DISTINCT sub_customvar_flat.id) >= ?)) OR host.id IS NULL)) AND (host.id IN ((SELECT sub_customvar_flat_host.id AS sub_customvar_flat_host_id FROM customvar_flat sub_customvar_flat INNER JOIN host_customvar sub_customvar_flat_t_host_customvar ON sub_customvar_flat_t_host_customvar.customvar_id = sub_customvar_flat.customvar_id INNER JOIN host sub_customvar_flat_host ON sub_customvar_flat_host.id = sub_customvar_flat_t_host_customvar.host_id WHERE (sub_customvar_flat.flatname = ?) AND (sub_customvar_flat.flatvalue = ?) GROUP BY sub_customvar_flat_host.id HAVING COUNT(DISTINCT sub_customvar_flat.id) >= ?))) ORDER BY host.id SQL; private const DOUBLE_EQUAL_RESULT = <<<'SQL' SELECT host.id FROM host WHERE host.id IN ((SELECT sub_customvar_flat_host.id AS sub_customvar_flat_host_id FROM customvar_flat sub_customvar_flat INNER JOIN host_customvar sub_customvar_flat_t_host_customvar ON sub_customvar_flat_t_host_customvar.customvar_id = sub_customvar_flat.customvar_id INNER JOIN host sub_customvar_flat_host ON sub_customvar_flat_host.id = sub_customvar_flat_t_host_customvar.host_id WHERE ((sub_customvar_flat.flatname = ?) AND (sub_customvar_flat.flatvalue = ?)) OR ((sub_customvar_flat.flatname = ?) AND (sub_customvar_flat.flatvalue = ?)) GROUP BY sub_customvar_flat_host.id HAVING COUNT(DISTINCT sub_customvar_flat.id) >= ?)) ORDER BY host.id SQL; /** @var Connection */ private $connection; public function setUp(): void { $this->connection = new TestConnection(); Backend::setDb($this->connection); $this->setUpSqlAssertions(); } public function testSingleUnequalCondition() { $query = Host::on($this->connection) ->columns('host.id') ->orderBy('host.id') ->filter(Filter::unequal('host.vars.in.valid', 'foo')); $this->assertSql(self::SINGLE_UNEQUAL_RESULT, $query->assembleSelect(), ['in.valid', 'foo', 1]); } public function testDoubleUnequalCondition() { $query = Host::on($this->connection) ->columns('host.id') ->orderBy('host.id') ->filter(Filter::unequal('host.vars.in.valid', 'foo')) ->filter(Filter::unequal('host.vars.missing', 'bar')); $this->assertSql( self::DOUBLE_UNEQUAL_RESULT, $query->assembleSelect(), ['in.valid', 'foo', 'missing', 'bar', 1] ); } public function testEqualAndUnequalCondition() { $query = Host::on($this->connection) ->columns('host.id') ->orderBy('host.id') ->filter(Filter::unequal('host.vars.in.valid', 'bar')) ->filter(Filter::equal('host.vars.env', 'foo')); $this->assertSql( self::EQUAL_UNEQUAL_RESULT, $query->assembleSelect(), ['in.valid', 'bar', 1, 'env', 'foo', 1] ); } public function testDoubleEqualCondition() { $query = Host::on($this->connection) ->columns('host.id') ->orderBy('host.id') ->filter(Filter::equal('host.vars.env', 'foo')) ->filter(Filter::equal('host.vars.os', 'bar')); $this->assertSql( self::DOUBLE_EQUAL_RESULT, $query->assembleSelect(), ['env', 'foo', 'os', 'bar', 2] ); } } icingadb-web-1.4.0/test/php/library/Icingadb/Model/CustomvarFlatTest.php000066400000000000000000000074161516124654200261620ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Modules\Icingadb\Model; use Icinga\Module\Icingadb\Model\CustomvarFlat; use PHPUnit\Framework\TestCase; class CustomvarFlatTest extends TestCase { public const EMPTY_TEST_SOURCE = [ ["dict.not_empty.foo","bar","dict","{\"empty\":{},\"not_empty\":{\"foo\":\"bar\"}}"], ["dict.empty",null,"dict","{\"empty\":{},\"not_empty\":{\"foo\":\"bar\"}}"], ["list[1]",null,"list","[[\"foo\",\"bar\"],[]]"], ["list[0][0]","foo","list","[[\"foo\",\"bar\"],[]]"], ["list[0][1]","bar","list","[[\"foo\",\"bar\"],[]]"], ["empty_list",null,"empty_list","[]"], ["empty_dict",null,"empty_dict","{}"], ["null","null","null","null"] ]; public const EMPTY_TEST_RESULT = [ "dict" => [ "not_empty" => [ "foo" => "bar" ], "empty" => [] ], "list" => [ ["foo", "bar"], [] ], "empty_list" => [], "empty_dict" => [], "null" => "null" ]; public const SPECIAL_CHAR_TEST_SOURCE = [ [ "vhosts.xxxxxxxxxxxxx.mgmt.xxxxxx.com.http_port", "443", "vhosts", "{\"xxxxxxxxxxxxx.mgmt.xxxxxx.com\":{\"http_port\":\"443\"}}" ], ["ex.ample.com.bla","blub","ex","{\"ample.com\":{\"bla\":\"blub\"}}"], ["example[1]","zyx","example[1]","\"zyx\""], ["example.0.org","xyz","example.0.org","\"xyz\""], ["ob.je.ct","***","ob","{\"je\":{\"ct\":\"tcejbo\"}}"], ["real_list[2]","three","real_list","[\"one\",\"two\",\"three\"]"], ["real_list[1]","two","real_list","[\"one\",\"two\",\"three\"]"], ["real_list[0]","one","real_list","[\"one\",\"two\",\"three\"]"], ["[1].2.[3].4.[5].6","123456","[1].2","{\"[3].4\":{\"[5].6\":123456}}"], ["ex.ample.com","cba","ex.ample.com","\"cba\""], ["[4]","four","[4]","\"four\""] ]; public const SPECIAL_CHAR_TEST_RESULT = [ "vhosts" => [ "xxxxxxxxxxxxx.mgmt.xxxxxx.com" => [ "http_port" => 443 ] ], "ex" => [ "ample.com" => [ "bla" => "blub" ] ], "example[1]" => "zyx", "example.0.org" => "xyz", "ob" => [ "je" => [ "ct" => "***" ] ], "real_list" => [ "one", "two", "three" ], "[1].2" => [ "[3].4" => [ "[5].6" => "123456" ] ], "ex.ample.com" => "cba", "[4]" => "four" ]; public function testUnflatteningOfEmptyCustomVariables() { $this->assertEquals( self::EMPTY_TEST_RESULT, (new CustomvarFlat())->unFlattenVars($this->transformSource(self::EMPTY_TEST_SOURCE)), "Empty custom variables are not correctly unflattened" ); } public function testUnflatteningOfCustomVariablesWithSpecialCharacters() { $this->assertEquals( self::SPECIAL_CHAR_TEST_RESULT, (new CustomvarFlat())->unFlattenVars($this->transformSource(self::SPECIAL_CHAR_TEST_SOURCE)), "Custom variables with special characters are not correctly unflattened" ); } protected function transformSource(array $source): \Generator { foreach ($source as $data) { yield (object) [ 'flatname' => $data[0], 'flatvalue' => $data[1], 'customvar' => (object) [ 'name' => $data[2], 'value' => $data[3] ] ]; } } } icingadb-web-1.4.0/test/php/library/Icingadb/ProvidedHook/000077500000000000000000000000001516124654200233445ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/library/Icingadb/ProvidedHook/IcingaHealthTest.php000066400000000000000000000012711516124654200272360ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Modules\Icingadb\ProvidedHook; use Icinga\Module\Icingadb\ProvidedHook\IcingaHealth; use PHPUnit\Framework\TestCase; class IcingaHealthTest extends TestCase { public function testNormalizeVersion() { $this->assertEquals('1.4.0', IcingaHealth::normalizeVersion('1.4.0')); $this->assertEquals('1.4.0', IcingaHealth::normalizeVersion('1.4.0-1d5a35da5')); $this->assertEquals('1.4.0', IcingaHealth::normalizeVersion('v1.4.0')); $this->assertEquals('1.4.0', IcingaHealth::normalizeVersion('v1.4.0-1d5a35da5')); } } icingadb-web-1.4.0/test/php/library/Icingadb/Util/000077500000000000000000000000001516124654200216645ustar00rootroot00000000000000icingadb-web-1.4.0/test/php/library/Icingadb/Util/PerfdataSetTest.php000066400000000000000000000077631516124654200254540ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Module\Icingadb\Util; use Icinga\Module\Icingadb\Util\PerfDataSet; use PHPUnit\Framework\TestCase; use Tests\Icinga\Module\Icingadb\Lib\PerfdataSetWithPublicData; class PerfdataSetTest extends TestCase { public function testWhetherValidSimplePerfdataLabelsAreProperlyParsed() { $pset = PerfdataSetWithPublicData::fromString('key1=val1 key2=val2 key3 =val3'); $this->assertSame( 'key1', $pset->perfdata[0]->getLabel(), 'PerfdataSet does not correctly parse valid simple labels' ); $this->assertSame( 'key2', $pset->perfdata[1]->getLabel(), 'PerfdataSet does not correctly parse valid simple labels' ); $this->assertSame( 'key3', $pset->perfdata[2]->getLabel(), 'PerfdataSet does not correctly parse valid simple labels' ); } public function testWhetherNonQuotedPerfdataLablesWithSpacesAreProperlyParsed() { $pset = PerfdataSetWithPublicData::fromString('key 1=val1 key 1 + 1=val2'); $this->assertSame( 'key 1', $pset->perfdata[0]->getLabel(), 'PerfdataSet does not correctly parse non quoted labels with spaces' ); $this->assertSame( 'key 1 + 1', $pset->perfdata[1]->getLabel(), 'PerfdataSet does not correctly parse non quoted labels with spaces' ); } public function testWhetherValidQuotedPerfdataLabelsAreProperlyParsed() { $pset = PerfdataSetWithPublicData::fromString('\'key 1\'=val1 "key 2"=val2 \'a=b\'=0%;;2'); $this->assertSame( 'key 1', $pset->perfdata[0]->getLabel(), 'PerfdataSet does not correctly parse valid quoted labels' ); $this->assertSame( 'key 2', $pset->perfdata[1]->getLabel(), 'PerfdataSet does not correctly parse valid quoted labels' ); $this->assertSame( 'a=b', $pset->perfdata[2]->getLabel(), 'PerfdataSet does not correctly parse labels with equal signs' ); } public function testWhetherInvalidQuotedPerfdataLabelsAreProperlyParsed() { $pset = PerfdataSetWithPublicData::fromString('\'key 1=1 key 2"=2'); $this->assertSame( 'key 1', $pset->perfdata[0]->getLabel(), 'PerfdataSet does not correctly parse invalid quoted labels' ); $this->assertSame( 'key 2"', $pset->perfdata[1]->getLabel(), 'PerfdataSet does not correctly parse invalid quoted labels' ); $pset = PerfdataSetWithPublicData::fromString('"key 1=1 "key 2"=2'); $this->assertSame( 'key 1=1', $pset->perfdata[0]->getLabel(), 'PerfdataSet does not correctly parse invalid quoted labels' ); $this->assertNull( $pset->perfdata[0]->getValue() ); $this->assertSame( '2"', $pset->perfdata[1]->getLabel(), 'PerfdataSet does not correctly parse invalid quoted labels' ); $this->assertSame( 2.0, $pset->perfdata[1]->getValue() ); } /** * @depends testWhetherValidSimplePerfdataLabelsAreProperlyParsed */ public function testWhetherAPerfdataSetIsIterable() { $pset = PerfdataSet::fromString('key=value'); foreach ($pset as $p) { $this->assertSame('key', $p->getLabel()); return; } $this->fail('PerfdataSet objects cannot be iterated'); } public function testWhetherPerfdataSetsCanBeInitializedWithEmptyStrings() { $pset = PerfdataSetWithPublicData::fromString(''); $this->assertEmpty($pset->perfdata, 'PerfdataSet::fromString does not accept emtpy strings'); } } icingadb-web-1.4.0/test/php/library/Icingadb/Util/PerfdataTest.php000066400000000000000000000466541516124654200250020ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Module\Icingadb\Util; use Icinga\Module\Icingadb\Util\PerfData; use PHPUnit\Framework\TestCase; class PerfdataTest extends TestCase { public function testWhetherFromStringThrowsExceptionWhenGivenAnEmptyString() { $this->expectException(\InvalidArgumentException::class); Perfdata::fromString(''); } public function testWhetherFromStringThrowsExceptionWhenGivenAnInvalidString() { $this->expectException(\InvalidArgumentException::class); Perfdata::fromString('test'); } public function testWhetherFromStringParsesAGivenStringCorrectly() { $p = Perfdata::fromString('key=1234'); $this->assertSame( 'key', $p->getLabel(), 'Perfdata::fromString does not properly parse performance data labels' ); $this->assertSame( 1234.0, $p->getValue(), 'Perfdata::fromString does not properly parse performance data values' ); } /** * @depends testWhetherFromStringParsesAGivenStringCorrectly */ public function testWhetherGetValueReturnsValidValues() { $this->assertSame( 1337.0, Perfdata::fromString('test=1337')->getValue(), 'Perfdata::getValue does not return correct values' ); $this->assertSame( 1337.0, Perfdata::fromString('test=1337;;;;')->getValue(), 'Perfdata::getValue does not return correct values' ); } /** * @depends testWhetherFromStringParsesAGivenStringCorrectly */ public function testWhetherDecimalValuesAreCorrectlyParsed() { $this->assertSame( 1337.5, Perfdata::fromString('test=1337.5')->getValue(), 'Perfdata objects do not parse decimal values correctly' ); $this->assertSame( 1337.5, Perfdata::fromString('test=1337.5B')->getValue(), 'Perfdata objects do not parse decimal values correctly' ); } /** * @depends testWhetherFromStringParsesAGivenStringCorrectly */ public function testWhetherGetValueReturnsNullForInvalidOrUnknownValues() { $this->assertNull( Perfdata::fromString('test=U')->getValue(), 'Perfdata::getValue does not return null for unknown values' ); $this->assertNull( Perfdata::fromString('test=i am not a value')->getValue(), 'Perfdata::getValue does not return null for invalid values' ); $this->assertNull( PerfData::fromString('test=')->getValue(), 'Perfdata::getValue does not return null for invalid values' ); $this->assertNull( PerfData::fromString('test=-kW')->getValue(), 'Perfdata::getValue does not return null for invalid values' ); $this->assertNull( PerfData::fromString('test=kW')->getValue(), 'Perfdata::getValue does not return null for invalid values' ); $this->assertNull( PerfData::fromString('test=-')->getValue(), 'Perfdata::getValue does not return null for invalid values' ); } /** * @depends testWhetherFromStringParsesAGivenStringCorrectly */ public function testWhetherUnitOfUnkownValuesIsCorrectlyIdentified() { $this->assertNull( Perfdata::fromString('test=U')->getUnit(), 'Perfdata::getUnit does not return null for unknown values' ); $this->assertNull( Perfdata::fromString('test=i am not a value')->getUnit(), 'Perfdata::getUnit does not return null for unknown values' ); $this->assertNull( PerfData::fromString('test=')->getUnit(), 'Perfdata::getUnit does not return null for unknown values' ); $this->assertSame( 'kW', PerfData::fromString('test=-kW')->getUnit(), 'Perfdata::getUnit does not return correct unit for invalid values' ); $this->assertSame( 'kW', PerfData::fromString('test=kW')->getUnit(), 'Perfdata::getUnit does not return correct unit for invalid values' ); $this->assertNull( PerfData::fromString('test=-')->getUnit(), 'Perfdata::getUnit does not return null for unknown values' ); } /** * @depends testWhetherFromStringParsesAGivenStringCorrectly */ public function testWhethergetWarningThresholdReturnsCorrectValues() { $zeroToTen = Perfdata::fromString('test=1;10')->getWarningThreshold(); $this->assertSame( 0.0, $zeroToTen->getMin(), 'Perfdata::getWarningThreshold does not return correct values' ); $this->assertSame( 10.0, $zeroToTen->getMax(), 'Perfdata::getWarningThreshold does not return correct values' ); $tenToInfinity = Perfdata::fromString('test=1;10:')->getWarningThreshold(); $this->assertSame( 10.0, $tenToInfinity->getMin(), 'Perfdata::getWarningThreshold does not return correct values' ); $this->assertNull( $tenToInfinity->getMax(), 'Perfdata::getWarningThreshold does not return correct values' ); $infinityToTen = Perfdata::fromString('test=1;~:10')->getWarningThreshold(); $this->assertNull( $infinityToTen->getMin(), 'Perfdata::getWarningThreshold does not return correct values' ); $this->assertSame( 10.0, $infinityToTen->getMax(), 'Perfdata::getWarningThreshold does not return correct values' ); $tenToTwenty = Perfdata::fromString('test=1;10:20')->getWarningThreshold(); $this->assertSame( 10.0, $tenToTwenty->getMin(), 'Perfdata::getWarningThreshold does not return correct values' ); $this->assertSame( 20.0, $tenToTwenty->getMax(), 'Perfdata::getWarningThreshold does not return correct values' ); $tenToTwentyInverted = Perfdata::fromString('test=1;@10:20')->getWarningThreshold(); $this->assertTrue( $tenToTwentyInverted->isInverted(), 'Perfdata::getWarningThreshold does not return correct values' ); } /** * @depends testWhetherFromStringParsesAGivenStringCorrectly */ public function testWhetherGetCriticalThresholdReturnsCorrectValues() { $zeroToTen = Perfdata::fromString('test=1;;10')->getCriticalThreshold(); $this->assertSame( 0.0, $zeroToTen->getMin(), 'Perfdata::getCriticalThreshold does not return correct values' ); $this->assertSame( 10.0, $zeroToTen->getMax(), 'Perfdata::getCriticalThreshold does not return correct values' ); $tenToInfinity = Perfdata::fromString('test=1;;10:')->getCriticalThreshold(); $this->assertSame( 10.0, $tenToInfinity->getMin(), 'Perfdata::getCriticalThreshold does not return correct values' ); $this->assertNull( $tenToInfinity->getMax(), 'Perfdata::getCriticalThreshold does not return correct values' ); $infinityToTen = Perfdata::fromString('test=1;;~:10')->getCriticalThreshold(); $this->assertNull( $infinityToTen->getMin(), 'Perfdata::getCriticalThreshold does not return correct values' ); $this->assertSame( 10.0, $infinityToTen->getMax(), 'Perfdata::getCriticalThreshold does not return correct values' ); $tenToTwenty = Perfdata::fromString('test=1;;10:20')->getCriticalThreshold(); $this->assertSame( 10.0, $tenToTwenty->getMin(), 'Perfdata::getCriticalThreshold does not return correct values' ); $this->assertSame( 20.0, $tenToTwenty->getMax(), 'Perfdata::getCriticalThreshold does not return correct values' ); $tenToTwentyInverted = Perfdata::fromString('test=1;;@10:20')->getCriticalThreshold(); $this->assertTrue( $tenToTwentyInverted->isInverted(), 'Perfdata::getCriticalThreshold does not return correct values' ); } /** * @depends testWhetherFromStringParsesAGivenStringCorrectly */ public function testWhetherGetMinimumValueReturnsCorrectValues() { $this->assertSame( 1337.0, Perfdata::fromString('test=1;;;1337')->getMinimumValue(), 'Perfdata::getMinimumValue does not return correct values' ); $this->assertSame( 1337.5, Perfdata::fromString('test=1;;;1337.5')->getMinimumValue(), 'Perfdata::getMinimumValue does not return correct values' ); } /** * @depends testWhetherFromStringParsesAGivenStringCorrectly */ public function testWhetherGetMaximumValueReturnsCorrectValues() { $this->assertSame( 1337.0, Perfdata::fromString('test=1;;;;1337')->getMaximumValue(), 'Perfdata::getMaximumValue does not return correct values' ); $this->assertSame( 1337.5, Perfdata::fromString('test=1;;;;1337.5')->getMaximumValue(), 'Perfdata::getMaximumValue does not return correct values' ); } /** * @depends testWhetherFromStringParsesAGivenStringCorrectly */ public function testWhetherMissingValuesAreProperlyHandled() { $perfdata = Perfdata::fromString('test=1;;3;5'); $this->assertEmpty( (string) $perfdata->getWarningThreshold(), 'Perfdata objects do not correctly identify omitted warning tresholds' ); $this->assertNull( $perfdata->getMaximumValue(), 'Perfdata objects do not return null for missing maximum values' ); } /** * @depends testWhetherGetValueReturnsValidValues */ public function testWhetherValuesAreIdentifiedAsNumber() { $this->assertTrue( Perfdata::fromString('test=666')->isNumber(), 'Perfdata objects do not identify ordinary digits as number' ); } /** * @depends testWhetherGetValueReturnsValidValues */ public function testWhetherValuesAreIdentifiedAsSeconds() { $this->assertTrue( Perfdata::fromString('test=666s')->isSeconds(), 'Perfdata objects do not identify seconds as seconds' ); } /** * @depends testWhetherGetValueReturnsValidValues */ public function testWhetherValuesAreIdentifiedAsPercentage() { $this->assertTrue( Perfdata::fromString('test=66%')->isPercentage(), 'Perfdata objects do not identify percentages as percentages' ); } /** * @depends testWhetherValuesAreIdentifiedAsPercentage */ public function testWhetherMinAndMaxAreNotRequiredIfUnitIsInPercent() { $perfdata = Perfdata::fromString('test=1%'); $this->assertSame( 0.0, $perfdata->getMinimumValue(), 'Perfdata objects do not set minimum value to 0 if UOM is %' ); $this->assertSame( 100.0, $perfdata->getMaximumValue(), 'Perfdata objects do not set maximum value to 100 if UOM is %' ); } /** * @depends testWhetherGetValueReturnsValidValues */ public function testWhetherValuesAreIdentifiedAsBytes() { $this->assertTrue( Perfdata::fromString('test=66666B')->isBytes(), 'Perfdata objects do not identify bytes as bytes' ); } /** * @depends testWhetherGetValueReturnsValidValues */ public function testWhetherValuesAreIdentifiedAsCounter() { $this->assertTrue( Perfdata::fromString('test=123c')->isCounter(), 'Perfdata objects do not identify counters as counters' ); } /** * @depends testWhetherValuesAreIdentifiedAsPercentage */ public function testWhetherPercentagesAreHandledCorrectly() { $this->assertSame( 66.0, Perfdata::fromString('test=66%')->getPercentage(), 'Perfdata objects do not correctly handle native percentages' ); $this->assertSame( 50.0, Perfdata::fromString('test=0;;;-250;250')->getPercentage(), 'Perfdata objects do not correctly convert suitable values to percentages' ); $this->assertNull( Perfdata::fromString('test=50')->getPercentage(), 'Perfdata objects do return a percentage though their unit is not % and no maximum is given' ); $this->assertNull( Perfdata::fromString('test=25;;;50;100')->getPercentage(), 'Perfdata objects do return a percentage though their value is lower than it\'s allowed minimum' ); $this->assertNull( Perfdata::fromString('test=25;;;0;')->getPercentage(), 'Perfdata objects do not ignore empty max values when returning percentages' ); $this->assertNull( Perfdata::fromString('test=25;;;0;0')->getPercentage(), 'Perfdata objects do not ignore impossible min/max combinations when returning percentages' ); } public function testWhetherInvalidValueInPerfDataHandledCorrectly() { $p1 = Perfdata::fromString('test=2,0'); $this->assertFalse($p1->isValid()); $this->assertNull( $p1->getValue(), 'Perfdata::getValue does not return null for invalid values' ); $this->assertSame( '2,0', $p1->toArray()['value'] ); $p2 = Perfdata::fromString('test=i am not a value'); $this->assertFalse($p2->isValid()); $this->assertNull( $p2->getValue(), 'Perfdata::getValue does not return null for invalid values' ); $this->assertSame( 'i am not a value', $p2->toArray()['value'] ); $p3 = Perfdata::fromString('test='); $this->assertFalse($p3->isValid()); $this->assertNull( $p3->getValue(), 'Perfdata::getValue does not return null for invalid values' ); $this->assertSame( '', $p3->toArray()['value'] ); $p4 = Perfdata::fromString('test=-kW'); $this->assertFalse($p4->isValid()); $this->assertNull( $p4->getValue(), 'Perfdata::getValue does not return null for invalid values' ); $this->assertSame( '-kW', $p4->toArray()['value'] ); $p5 = Perfdata::fromString('test=kW'); $this->assertFalse($p5->isValid()); $this->assertNull( $p5->getValue(), 'Perfdata::getValue does not return null for invalid values' ); $this->assertSame( 'kW', $p5->toArray()['value'] ); $p6 = Perfdata::fromString('test=-'); $this->assertFalse($p6->isValid()); $this->assertNull( $p6->getValue(), 'Perfdata::getValue does not return null for invalid values' ); $this->assertSame( '-', $p6->toArray()['value'] ); } public function testWhetherInvalidMinInPerfDataHandledCorrectly() { $p1 = Perfdata::fromString('test=1;;;2,0'); $this->assertFalse($p1->isValid()); $this->assertNull( $p1->getMinimumValue(), 'Perfdata::getMinimumValue does not return null for invalid min values' ); $this->assertSame( '2,0', $p1->toArray()['min'] ); $p2 = Perfdata::fromString('test=1;;;foo'); $this->assertFalse($p2->isValid()); $this->assertNull( $p2->getMinimumValue(), 'Perfdata::getMinimumValue does not return null for invalid min values' ); $this->assertSame( 'foo', $p2->toArray()['min'] ); } public function testWhetherInvalidMaxInPerfDataHandledCorrectly() { $p1 = Perfdata::fromString('test=1;;;;2,0'); $this->assertFalse($p1->isValid()); $this->assertNull( $p1->getMaximumValue(), 'Perfdata::getMaximumValue does not return null for invalid max values' ); $this->assertSame( '2,0', $p1->toArray()['max'] ); $p2 = Perfdata::fromString('test=1;;;;foo'); $this->assertFalse($p2->isValid()); $this->assertNull( $p2->getMaximumValue(), 'Perfdata::getMaximumValue does not return null for invalid max values' ); $this->assertSame( 'foo', $p2->toArray()['max'] ); } public function testWhetherInvalidWarningThresholdInPerfDataHandledCorrectly() { $p1 = Perfdata::fromString('test=1;2,0:'); $this->assertFalse($p1->getWarningThreshold()->isValid()); $this->assertFalse($p1->isValid()); $this->assertSame( '2,0:', (string) $p1->getWarningThreshold() ); $p2 = Perfdata::fromString('test=1;0:4,0'); $this->assertFalse($p2->getWarningThreshold()->isValid()); $this->assertFalse($p2->isValid()); $this->assertSame( '0:4,0', (string) $p2->getWarningThreshold() ); $p3 = Perfdata::fromString('test=1;foo'); $this->assertFalse($p2->getWarningThreshold()->isValid()); $this->assertFalse($p3->isValid()); $this->assertSame( 'foo', (string) $p3->getWarningThreshold() ); $p4 = Perfdata::fromString('test=1;10@'); $this->assertFalse($p2->getWarningThreshold()->isValid()); $this->assertFalse($p4->isValid()); $this->assertSame( '10@', (string) $p4->getWarningThreshold() ); } public function testWhetherInvalidCriticalThresholdInPerfDataHandledCorrectly() { $p1 = Perfdata::fromString('test=1;;2,0:'); $this->assertFalse($p1->getCriticalThreshold()->isValid()); $this->assertFalse($p1->isValid()); $this->assertSame( '2,0:', (string) $p1->getCriticalThreshold() ); $p2 = Perfdata::fromString('test=1;;0:4,0'); $this->assertFalse($p2->getCriticalThreshold()->isValid()); $this->assertFalse($p2->isValid()); $this->assertSame( '0:4,0', (string) $p2->getCriticalThreshold() ); $p3 = Perfdata::fromString('test=1;;foo'); $this->assertFalse($p2->getCriticalThreshold()->isValid()); $this->assertFalse($p3->isValid()); $this->assertSame( 'foo', (string) $p3->getCriticalThreshold() ); $p4 = Perfdata::fromString('test=1;;10@'); $this->assertFalse($p2->getCriticalThreshold()->isValid()); $this->assertFalse($p4->isValid()); $this->assertSame( '10@', (string) $p4->getCriticalThreshold() ); } } icingadb-web-1.4.0/test/php/library/Icingadb/Util/PluginOutputTest.php000066400000000000000000000131131516124654200257130ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Module\Icingadb\Util; use Icinga\Module\Icingadb\Util\PluginOutput; use PHPUnit\Framework\TestCase; class PluginOutputTest extends TestCase { public function checkOutput(string $expected, string $input, int $characterLimit = 0): void { $p = new PluginOutput($input); if ($characterLimit) { $p->setCharacterLimit($characterLimit); } $this->assertSame($expected, $p->render(), 'PluginOutput::render does not return expected values'); } public function testRenderPlainText(): void { $input = 'This is a plain text'; $this->checkOutput($input, $input); } public function testRenderTextWithStates(): void { $input = <<<'INPUT' [OK] Dummy state \_ [OK] Fake "state" \_ [WARNING] Fake state again INPUT; $expectedOutput = <<<'EXPECTED_OUTPUT' Dummy state \_ Fake "state" \_ Fake state again EXPECTED_OUTPUT; $this->checkOutput($expectedOutput, $input); } public function testRenderTextWithStatesAndCharacterLimit(): void { $input = <<<'INPUT' [OK] Dummy state \_ [OK] Fake "state" \_ [WARNING] Fake state again INPUT; $expectedOutput = <<<'EXPECTED_OUTPUT' Dummy EXPECTED_OUTPUT; $this->checkOutput($expectedOutput, $input, 10); } public function testRenderTextWithHtml(): void { $input = <<<'INPUT' Hello

World

, this "is" 'a test. INPUT; $expectedOutput = <<<'EXPECTED_OUTPUT' Hello

World

, this "is" 'a test. EXPECTED_OUTPUT; $this->checkOutput($expectedOutput, $input); } public function testRenderTextWithHtmlAndStates(): void { $input = <<<'INPUT' Hello

World

, this "is" a test. [OK] Dummy state \_ [OK] Fake "state" \_ [WARNING] Fake state again text ends here INPUT; $expectedOutput = <<<'EXPECTED_OUTPUT' Hello

World

, this "is" a test. Dummy state \_ Fake "state" \_ Fake state again text ends here EXPECTED_OUTPUT; $this->checkOutput($expectedOutput, $input); } public function testRenderTextWithHtmlIncludingStatesAndSpecialChars(): void { $input = <<<'INPUT' Hello

World

, this "is" a test. [OK] Dummy state special chars: !@#$%^&*()_+{}|:"<>?`-=[]\;',./ text ends here INPUT; $expectedOutput = <<<'EXPECTED_OUTPUT' Hello

World

, this "is" a test. Dummy state special chars: !@#$%^&*()_+{}|:"<>?`-=[]\;',​./ text ends here EXPECTED_OUTPUT; $this->checkOutput($expectedOutput, $input); } public function testOutputWithNewlines(): void { $input = 'foo\nbar\n\nraboof'; $expectedOutput = "foo\nbar\n\nraboof"; $this->checkOutput($expectedOutput, $input); } public function testOutputWithHtmlEntities(): void { $input = 'foo & bar'; $expectedOutput = $input; $this->checkOutput($expectedOutput, $input); } public function testSimpleHtmlOutput(): void { $input = <<<'INPUT' OK - Teststatus
Info INPUT; $expectedOutput = <<<'EXPECTED_OUTPUT' OK - Teststatus Info EXPECTED_OUTPUT; $this->checkOutput($expectedOutput, $input); } public function testTextStatusTags(): void { foreach (['OK', 'WARNING', 'CRITICAL', 'UNKNOWN', 'UP', 'DOWN'] as $s) { $l = strtolower($s); $input = sprintf('[%s] Test', $s); $expectedOutput = sprintf(' Test', $l); $this->checkOutput($expectedOutput, $input); $input = sprintf('(%s) Test', $s); $this->checkOutput($expectedOutput, $input); } } public function testHtmlStatusTags(): void { $dummyHtml = ''; foreach (['OK', 'WARNING', 'CRITICAL', 'UNKNOWN', 'UP', 'DOWN'] as $s) { $l = strtolower($s); $input = sprintf('%s [%s] Test', $dummyHtml, $s); $expectedOutput = sprintf('%s Test', $dummyHtml, $l); $this->checkOutput($expectedOutput, $input); $input = sprintf('%s (%s) Test', $dummyHtml, $s); $this->checkOutput($expectedOutput, $input); } } public function testNewlineProcessingInHtmlOutput(): void { $input = 'This is plugin output\n\n
    \n
  • with a HTML list
  • \n
\n\n' . 'and more text that\nis split onto multiple\n\nlines'; $expectedOutput = <<<'EXPECTED_OUTPUT' This is plugin output
  • with a HTML list
and more text that is split onto multiple lines EXPECTED_OUTPUT; $this->checkOutput($expectedOutput, $input); } } icingadb-web-1.4.0/test/php/library/Icingadb/Util/ThresholdRangeTest.php000066400000000000000000000313631516124654200261540ustar00rootroot00000000000000 // SPDX-License-Identifier: GPL-3.0-or-later namespace Tests\Icinga\Module\Icingadb\Util; use Icinga\Module\Icingadb\Util\ThresholdRange; use PHPUnit\Framework\TestCase; class ThresholdRangeTest extends TestCase { public function testFromStringProperlyParsesDoubleExclusiveRanges() { $outside0And10 = ThresholdRange::fromString('10'); $this->assertSame( 0.0, $outside0And10->getMin(), 'ThresholdRange::fromString() does not identify zero as default minimum for double exclusive ranges' ); $this->assertSame( 10.0, $outside0And10->getMax(), 'ThresholdRange::fromString() does not identify ten as explicit maximum for double exclusive ranges' ); $this->assertFalse( $outside0And10->isInverted(), 'ThresholdRange::fromString() identifies double exclusive ranges as inclusive' ); $outside10And20 = ThresholdRange::fromString('10:20'); $this->assertSame( 10.0, $outside10And20->getMin(), 'ThresholdRange::fromString() does not identify ten as explicit minimum for double exclusive ranges' ); $this->assertSame( 20.0, $outside10And20->getMax(), 'ThresholdRange::fromString() does not identify twenty as explicit maximum for double exclusive ranges' ); $this->assertFalse( $outside10And20->isInverted(), 'ThresholdRange::fromString() identifies double exclusive ranges as inclusive' ); } /** * @depends testFromStringProperlyParsesDoubleExclusiveRanges */ public function testContainsCorrectlyEvaluatesDoubleExclusiveRanges() { $outside0And10 = ThresholdRange::fromString('10'); $this->assertFalse( $outside0And10->contains(-1), 'ThresholdRange::contains() identifies negative values as greater than or equal to zero' ); $this->assertFalse( $outside0And10->contains(11), 'ThresholdRange::contains() identifies eleven as smaller than or equal to ten' ); $this->assertTrue( $outside0And10->contains(10), 'ThresholdRange::contains() identifies 10 as outside the range 0..10' ); $outside10And20 = ThresholdRange::fromString('10:20'); $this->assertFalse( $outside10And20->contains(9), 'ThresholdRange::contains() identifies nine as greater than or equal to 10' ); $this->assertFalse( $outside10And20->contains(21), 'ThresholdRange::contains() identifies twenty-one as smaller than or equal to twenty' ); $this->assertTrue( $outside10And20->contains(20), 'ThresholdRange::contains() identifies 20 as outside the range 10..20' ); } public function testFromStringProperlyParsesSingleExclusiveRanges() { $smallerThan10 = ThresholdRange::fromString('10:'); $this->assertSame( 10.0, $smallerThan10->getMin(), 'ThresholdRange::fromString() does not identify ten as explicit minimum for single exclusive ranges' ); $this->assertNull( $smallerThan10->getMax(), 'ThresholdRange::fromString() does not identify infinity as default maximum for single exclusive ranges' ); $this->assertFalse( $smallerThan10->isInverted(), 'ThresholdRange::fromString() identifies single exclusive ranges as inclusive' ); $greaterThan10 = ThresholdRange::fromString('~:10'); $this->assertNull( $greaterThan10->getMin(), 'ThresholdRange::fromString() does not identify infinity as explicit minimum for single exclusive ranges' ); $this->assertSame( 10.0, $greaterThan10->getMax(), 'ThresholdRange::fromString() does not identify ten as explicit maximum for single exclusive ranges' ); $this->assertFalse( $greaterThan10->isInverted(), 'ThresholdRange::fromString() identifies single exclusive ranges as inclusive' ); } /** * @depends testFromStringProperlyParsesSingleExclusiveRanges */ public function testContainsCorrectlyEvaluatesSingleExclusiveRanges() { $smallerThan10 = ThresholdRange::fromString('10:'); $this->assertFalse( $smallerThan10->contains(9), 'ThresholdRange::contains() identifies nine as greater than or equal to ten' ); $this->assertTrue( $smallerThan10->contains(PHP_INT_MAX), 'ThresholdRange::contains() identifies infinity as outside the range 10..~' ); $greaterThan10 = ThresholdRange::fromString('~:10'); $this->assertFalse( $greaterThan10->contains(11), 'ThresholdRange::contains() identifies eleven as smaller than or equal to ten' ); $this->assertTrue( $greaterThan10->contains(~PHP_INT_MAX), 'ThresholdRange::contains() identifies negative infinity as outside the range ~..10' ); } public function testFromStringProperlyParsesInclusiveRanges() { $inside0And10 = ThresholdRange::fromString('@10'); $this->assertSame( 0.0, $inside0And10->getMin(), 'ThresholdRange::fromString() does not identify zero as default minimum for inclusive ranges' ); $this->assertSame( 10.0, $inside0And10->getMax(), 'ThresholdRange::fromString() does not identify ten as explicit maximum for inclusive ranges' ); $this->assertTrue( $inside0And10->isInverted(), 'ThresholdRange::fromString() identifies inclusive ranges as double exclusive' ); $inside10And20 = ThresholdRange::fromString('@10:20'); $this->assertSame( 10.0, $inside10And20->getMin(), 'ThresholdRange::fromString() does not identify ten as explicit minimum for inclusive ranges' ); $this->assertSame( 20.0, $inside10And20->getMax(), 'ThresholdRange::fromString() does not identify twenty as explicit maximum for inclusive ranges' ); $this->assertTrue( $inside10And20->isInverted(), 'ThresholdRange::fromString() identifies inclusive ranges as double exclusive' ); $greaterThan10 = ThresholdRange::fromString('@10:'); $this->assertSame( 10.0, $greaterThan10->getMin(), 'ThresholdRange::fromString() does not identify ten as explicit minimum for inclusive ranges' ); $this->assertNull( $greaterThan10->getMax(), 'ThresholdRange::fromString() does not identify infinity as default maximum for inclusive ranges' ); $this->assertTrue( $greaterThan10->isInverted(), 'ThresholdRange::fromString() identifies inclusive ranges as single exclusive' ); $smallerThan10 = ThresholdRange::fromString('@~:10'); $this->assertNull( $smallerThan10->getMin(), 'ThresholdRange::fromString() does not identify infinity as explicit minimum for inclusive ranges' ); $this->assertSame( 10.0, $smallerThan10->getMax(), 'ThresholdRange::fromString() does not identify ten as explicit maximum for inclusive ranges' ); $this->assertTrue( $smallerThan10->isInverted(), 'ThresholdRange::fromString() identifies inclusive ranges as single exclusive' ); } /** * @depends testFromStringProperlyParsesInclusiveRanges */ public function testContainsCorrectlyEvaluatesInclusiveRanges() { $inside0And10 = ThresholdRange::fromString('@10'); $this->assertFalse( $inside0And10->contains(10), 'ThresholdRange::contains() identifies ten as greater than ten' ); $this->assertTrue( $inside0And10->contains(11), 'ThresholdRange::contains() identifies eleven as smaller than or equal to ten' ); $this->assertTrue( $inside0And10->contains(-1), 'ThresholdRange::contains() identifies negative values as greater than or equal to zero' ); $inside10And20 = ThresholdRange::fromString('@10:20'); $this->assertFalse( $inside10And20->contains(20), 'ThresholdRange::contains() identifies twenty as greater than twenty' ); $this->assertTrue( $inside10And20->contains(21), 'ThresholdRange::contains() identifies twenty-one as smaller than or equal to twenty' ); $this->assertTrue( $inside10And20->contains(9), 'ThresholdRange::contains() identifies nine as greater than or equal to ten' ); $greaterThan10 = ThresholdRange::fromString('@10:'); $this->assertFalse( $greaterThan10->contains(PHP_INT_MAX), 'ThresholdRange::contains() identifies infinity as smaller than ten' ); $this->assertTrue( $greaterThan10->contains(9), 'ThresholdRange::contains() identifies nine as greater than or equal to ten' ); $smallerThan10 = ThresholdRange::fromString('@~:10'); $this->assertFalse( $smallerThan10->contains(~PHP_INT_MAX), 'ThresholdRange::contains() identifies negative infinity as greater than ten' ); $this->assertTrue( $smallerThan10->contains(11), 'ThresholdRange::contains() identifies eleven as smaller than or equal to ten' ); } public function testFromStringProperlyParsesEmptyThresholds() { $emptyThreshold = ThresholdRange::fromString(''); $this->assertNull( $emptyThreshold->getMin(), 'ThresholdRange::fromString() does not identify negative infinity as implicit minimum for empty strings' ); $this->assertNull( $emptyThreshold->getMax(), 'ThresholdRange::fromString() does not identify infinity as implicit maximum for empty strings' ); $this->assertFalse( $emptyThreshold->isInverted(), 'ThresholdRange::fromString() identifies empty strings as inclusive ranges rather than exclusive' ); } /** * @depends testFromStringProperlyParsesEmptyThresholds */ public function testContainsEvaluatesEverythingToTrueForEmptyThresholds() { $emptyThreshold = ThresholdRange::fromString(''); $this->assertTrue( $emptyThreshold->contains(0), 'ThresholdRange::contains() does not identify zero as valid without any threshold' ); $this->assertTrue( $emptyThreshold->contains(10), 'ThresholdRange::contains() does not identify ten as valid without any threshold' ); $this->assertTrue( $emptyThreshold->contains(PHP_INT_MAX), 'ThresholdRange::contains() does not identify infinity as valid without any threshold' ); $this->assertTrue( $emptyThreshold->contains(~PHP_INT_MAX), 'ThresholdRange::contains() does not identify negative infinity as valid without any threshold' ); } public function testInvalidThresholdNotationsAreRenderedAsIs() { $this->assertSame( ':', (string) ThresholdRange::fromString(':') ); $this->assertSame( '~:', (string) ThresholdRange::fromString('~:') ); $this->assertSame( '20:10', (string) ThresholdRange::fromString('20:10') ); $this->assertSame( '10@', (string) ThresholdRange::fromString('10@') ); $this->assertSame( 'foo', (string) ThresholdRange::fromString('foo') ); $this->assertSame( '4,4:2,2', (string) ThresholdRange::fromString('4,4:2,2') ); } public function testInvalidThresholdNotationsConsideredInValid() { $this->assertFalse( ThresholdRange::fromString('10@')->isValid(), 'Invalid threshold notation 10@ considered as valid' ); $this->assertFalse( ThresholdRange::fromString('foo')->isValid(), 'Invalid threshold notation foo considered as valid' ); $this->assertFalse( ThresholdRange::fromString('4,4:2,2')->isValid(), 'Invalid threshold notation 4,4:2,2 considered as valid' ); } }

M!-$׷R$;NI%$)P@P@P@:t x=CJ#$m ݤ/myl̷FFt`5_'..ԟyAi_{Ψc2=13m[JWe Iẵ;kyRk{ydx&r,lG$lJhj^ b2VHdIc`pU`ACA Ҁ 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7jѭ3w]6:s##jP@|+O 4I#);KG bGG[6])-j)m>4},xض,-K 8>J}؋-|O(N ( uƙiڱK+"=DᶷLRE&>ఽQ-`,D>óC@.Kw,JXiA$Eٳ`dZEPphz( ( ( ( ( (9)`(xHE)$RऑȌQсVV*oC-⿆uυßM#x:56wfv]Hfwytu`(s'e)"A,zBrXhoҼC>DSJy}F#Hs 4r7ikFd.q,c0™ym\"V6YYß&,HS>PP쥶|.,v&szYs:P@w_ e+i$ӰV pfܪ8h?#]е=$FxvpevID&p9*'4]ͅWմ 0ȇ˕u!`Z=n&ԦY5#s.gdFIT@ZIh\{q.m%9<'PìYXpU  2 6 (σvS[nU*! 3ۋEkJP@ Ɋymi,G+`2N3(Ti(WX5/$*""*I8 Նp7wz巘8&P1Ru;XP|mKMPE&igaGKi`h?0TP@--fC%[ƒ$8O,÷$G`F(?(P@P@P@P@P@P@P@P@P@P@P@P@P@R֫c:2#rybg2:@P@P@P@ߋ|#x@׉l#4B=F,J0Mז|{dndgFς"2'5j[sf7'v}f'5Ŀ-1җ.tS .RWZ| Zkۨ-O42B%URf>; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j$^-+(PioN2>8z( ( (Oy=]=vDcyb_3DCu$3C"XRFxeu%Xw>wJ'^YK+v?ivqG0g`K=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN }+/'A ]"%7!I#wĽp>д;iizte ,O;UUH,h@RRY 9)#WFT?2!mP 7v#ƀ;] :'h̀ bO%A$@HI(ޠ[Şn,٠K+4eNJܻ©VI>|M YY-mmu(vMm{m+7%Y@uv ?ʞjG.%T Pxze-ܧ"o#p3V>cz__2ܫobC]W @/xĻ_T r/mݠU™ST\βJMr#i0Eԯ°a y9#" WuGvV6A"=.!;{hQcQc(aR8 K@1_cXV&጗Wy3Hrң!YZ&/˛w2.>=°utb95Ӭс yhІe2HBܠEZ[XZeg AkkC1,q¨I8噉fbY3@2#:d`YXa 0G94w`e/È<1  2DbV-@,cl @v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j|]n?mo;ڀ9( ( (;O۽MٷtZ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j$Y5++@wo3J>8z( ( (OMg=vwdxo0P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@PaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7jѢ3 3!闬]=:s;rP@P@P:SӭTVp/#c X@pXYCXYZkzXV!Ac݉' tP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@PaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7j cJ*w[1mLeT.ִ{Sd4GɇCgkue ( (=7x"H4tq2vUz( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (K?4( ( ( ( ( (?( ( ( ( ( ( ( w?7j8_x&ŶAhk.KK[ĕ` H9@>OԴ"k F[Kta9c-(( (3Ejo[trO:2Zbk1@w 8cQ"$H5DE\*(  b@ekzŞE{ $PIes1TWuS˻{}*qᴶeOfs$̋ ȉAg? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  C?f4^ w}[#1`Ԣ@VC8LBVg"4%rՑH= 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j AU2.";bL3m$pZ' Ex_ә5A -^x$.b0HHA"U >O&^uuo- *!?N@P PSx+/ش c* _ < &WV=5ץMZv"иVeBA۾(*UU@UU*8 3G|Zjvw7ArѼ|Un.- cwI( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (msyekf\]A 211; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ()4q꒢ !3[t$lsVɀ9-i:U git6v1FEhP@P@O70oqM3E"HRH G4:m25{6bkQ,y76EYgŽ(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@߄_ %u0wp[-n3*H˴IՀ= 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( (9_xc@VKkcǻw_";9u*[r|pZʼnBU31FE$DQQPuP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@oZikuw5Ƌq4p\O#H#6ٌ&Cec5ee.#x>; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ( ( (oY'?QZԦ Hn-n"Wf|,ݚtFKXxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=ct-#Bt{܃!IR)g/4A;|_nN1Z 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( (>;j~[ 6_oeo Bڋ-5Z HJ4u9̓f.~|2K1neGWP]:VrwG&*7jƿ:N*)&Ԁ>Ҡ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( (j/چխd<_"+? RMk5it(mTXsp_s{?oO;}&MA?4. mvm6cb]HZ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ( ( ( ( ( ( (/ڋ-eeYi>K,^Ok7;4)6]k7ic ?s_yu}wǏomVXqߦ'4ThRf ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ,~ i^wo HK>ivܽ\+K#w^!kݸ7.nj $w.JmYȴu @-lhZJ{P( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (k ( ( ( ( ( (( ( ( ( ( ( ( B+H A 8 }ZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@տK4ok 7A5cֵ;47f'L"{!򭢿e/:>8'~>˨_ lfUM wHzu/JT얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵȉETQ*(U982I's: ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( (>??j+e Y|gk{iky6?k>NBcŢ++R5_:޷?h5 BHo<Є};NtͧѴ+c{ybcآ@XȮn5@h ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (jګG[=c<`g_G5{پky[XXC5 <e]c:ǿW&MBi>Ǻkc+cusbc"@i]\@|@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P( ( ( ( ( ( ( ( ( ( ( ( ( (oګ?g Fmſ<\g_Z$ח\]lub̛,  :ncT>xPDZ+<mw,3ield{%[C[P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P( ( ( ( ( ( ( ( ( ( ( ( (>Njo_|WZ\]\KK}_V&=+!fih~߲ψ4}~_|XQLW6GIHԳ)e4[X*:P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( xv~7zM/~WO i5xΗ?a>&jAwJV{b,oHD>% ^ڏ ׋/ )uoYkQhUEy >;di Db5 eH~ awᏃ>0'%>F[g3qg{k([;Va&8oVN+iQ@=2 (D?eߍ75GWi.Я<-/Km7Qi7gH۽̋}g-@Ow<' +ž-c+o:·sr֯u8|GՏ¾"ү5e¶P@P@P@)~ߵ/?g_ [Zۧ>)xM< mOu{u|L hkY g<_}9"Zڿʷ1-ƟpZ7#__&)sWK7iz6~Gƺ?hWG_ _>_7-`ᴳѯ5-]R :-NkKM>=:?e + 1_u |2k^97xTΡÚ]εSzf?mAߵh.~3@lo^x|lh~I/ƍi >5/~)Uhַ?kzFN8ZKީB,6߲OZ|BП~~ԺOO|đ^x_bѼqy-ƞ|gk.0?_joڃmtOt߄Ʒ5(MF :gіQopI/Ucq9 kQg |O/k>!>#iS\h֩[YS75Iw ;@xSԚd޷;jg?j_ hs>*Cow{x\.'5{m+ooUx%5+MTiC?RҠ.Gٿw?)|u7|8%C_/j$(ߎu |=7.+v \2c i>eeI>)|2🋵*ɥ{-3^Դ}Lcg%LmM!.XдP@Pʿ/G$P'& < ldPOêړuotDX-ޯxٚae|rKhf#m;eͭ KK{kT[O تBU  ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (?( ( ( ( ( ( ( (]į"CƟ߳GƭGƶֺƓ`#mY?]ƺXifCӮ5 wKh,`~^| cYzixg^<w:[zV{M#xsLt_JW=dcǿSoE?g_Oş=UEþ8Y8x'tMoi~S=žokk?/=2WDj?P kwo-"|S e%񣾍g8wPtZ+KEeWikz*~?wӯ@](??c/ث@GLx{O.tG2;h> ״[FA D=~~Vx:N.@0^%n :|YO6*yCo>g'it}fWo?3 _FG|+k_+ɨxR.O^>_7]FMVH4O'ӭnk;cгWj?p쵨Z{[~ʷ^ (2ԕ`O4x?$OcSƱo\|owK>#v !HE޶|k_ NFeokYZ6osT?WڷVbuJx s|K|# 66zxBnk:泥4 k[@]~h^g;< lI'ovM~`u_ C(hYPMP@|Q^- 0>$xgSoB+@]j@inf7@+-'WIjJZ==c6vV궾 il?L!@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( '@_xSx׍|;x@L/ukt$ۻ!$ <3Q|'4^4i/?oOGx/LRMVB]TCeBm#JCDt?Zuk;;U5k+mGK B)/lwa'hFFa@S}e7߳O_;5_xÿYÚY߈<9ڳ[_x~gmɢgWw EC'__1x^$k; <;NkGχE">mV/ ]~Ѩj71YhVK;em@_N?7G_>~?:=Q3@įF_OXiz'ltk6{5?6HZ;[۠炼 t*~_ xsM,4MLO#/b%wY/WaK 2 ECYjP4*?'h |ic^n~m} oi~$q[GJ#w@/ ?~ |,O Vo·6hm3<#R@,xy |=㏉>Bc>/7S[WAf^@5_H~l4N{/&i&`GYH_?=mjV ?OEm@=O-P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( (8_?|F_/5H4[]KVy#T Qy-4e9x/nlog&(x/nlog&(x/nlog&(x/nlog&(x/nlog&(-~|S>+Ҿ'Oτ{+jAmNqleҭ13]ܓmq::rM2?>2Q߇5(Yk4}^Q|Fw$j]D@)1~??c{cO 7k{+:i#|3˭%SYii^$%R:d SڃiOC~~6u/Ě,nTlePb'g@REVP_w췣 ҟw% ~1|]/ 4`f b${?{e_'$vK\|@vi+i3M e##L;}@P@P@P@P@P@it r-U]Gg+d:Lt$$XS#&`'#%|'_?oKWl~kO]wO `WzL߹cB('9|/{~q|е7޵}jbm6d‰i ıuѿ~7s?ڏèti?eK/o^;:j7?|/X%X^'uRSYjD xkH~vz]CƯ>OM>.`o|&Ӽ{vfO ӼCy7`ieH٨Y(';|o{>wc ^ohjV lɽ泩]Zy$DΈ"wE}hר ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( |_{uO ?úC ծu=%VrƯEdlaԩ!??%cQo,sw;]K?<|8Oލ??|!/x/&XL(孬0{(` bi(~1q{W~(^_|1wë]/OѠ}xֺ헃lT֞Ӵn\7qh\y+p}/Bx_5dy"Mo.<< HQٴC *a%χ4c?<Q| 0ƓNBJb%GS@[Kl/_W%>"Dt WM._ kV4N] rfbcS~U|5H繎h~j*g1EiuGi>.<5^GYKᮄc i5xJrNDF1n6~ Ѽ_|ݲU,_ĺݫ|A)]nL) ߵNR~M׿lu/^wQxů Cx:U]m.u:%Z?_Z¯?`Gj_xއh~&lSVm_I tYkh<;f}'&_6A| cS XǖvioF׼MWP6Rۘڀ?uoWş_PGr9iiK5KGMj/-/I$Bno 4W1/$Pu-4yK%g~_5~ֺnj=xWz?OeiglW7}ZMG[S^ #5kwmk:xr?ǎ?l|RaxO>񍄚xGM6c/kwח:ou]:H@toS].(Ž[(_2٩Ckm|QE166~#|5^x9^I\PI#h,N ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (4Og6j?`KLW6e?z V-kgoۺٝ :C2ۮ#X䷖TPfI? ,[<^wj7cw]ֵsOYY 6s[u |cl>&Gxg'ÿ뷖Na焼6PxgDlnU ]+R-s ^\t?i/` ƿb-ǀ~*:դV7d._Ky}7v_|z&K-?=1 K᷄E?i߇)E}M{{uO=Ʋo 6e6cq$Ty a@?vnس?'N0xg*j+?,u+-nľ(᝼^&𞻮 OWx-Y _Cllu?]W |vai^8KZ?&Ҵ$IOZYj^ 9"B)a77]/xR^~.*ǁh`Vx烴]cźe{e?QJk}7V< g`ڀ~v~Kk>O?]/_^w-ƾ#O=z~]]-:)=_*//%q?{~DeW|Jtmc~ 툵=8Voot>آJ[|vS? |i_>E6iuEzizjڗ_Vݶn$Tmw#ZPय़J3U~s_|d!x+>_ g\ִsT= GĶ>2}3Ojz&kwk:i;Wogǟ5ƞx"o4uPDx#C< hw>'CiƇCZL%Ɠm=j?^?߅|1⟋ZwX/_+k iaYhڅ֡Z6qǭiǤũkOڸ)~x1a>' GOx>!5OO<_L-h#MZ4{=4ygG!|tOy5~ܾnH> j)Mx6_'|=i&m˦sey܉mD?'wGgkŐko xEO/մ rUӥ-!ymn-#S ѻ~P@P@P@P@P@P@Ku/>_x GOtڭW7^Y=iuk4WDvg/ C67{Pgƫ?X|5~V\:":ea,:ۥfT?ϟO+6||oAxN_*-  '_)m;~/OaͨYji^P2k>>1xwPھkF%C_ ";\Vm84}WFY'^ѯ[K46]+AJ77? -!.O9K` K5{.>kM=W:fj׾nHe^6Gq^@]_ Xk~_*>|O*=jğ~_Ͳ,fW>b ^Y-m l(.,@??_^1_ hgQkXhV#%kok&mge1\-$q*]@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@{nn|3[Oq|F3"'7|]O_>#xD gËzƫe}|R@a>DW3r}?.|n-f཭_`s [4ǣ7!hA$z5e+:i0/>3~_f/߳OljW}y麖5o]hxgþ+|x'Mqa ,\ǡ}e1|~o!5Ю|C/HmFIO xWI[6?]?/OU6~߳em?C:މ⿆MCV Ljo4=u|P-J/m}4ހqT_,h?iO:2OҼO/:^4-^9<hfJѯ};}Ώgо0k'S\ ]7ᴾ7Ѽԭl/|KXYikrM̐KynWKkF{ Sm*BZ.IuR4]bQM>;k{[etucppA@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( (r|@Uңּs$YO;{[XHC}Gv0ǦRkv7/o~]b^gI[[ɑ, ]\G ՝SZ]C2FP@P@P@P@P@P@5M/MY4ROԴB+ KYḷ) ~J=xx][_njX?3`K_]ȐEuk,DODn$HuHV xxE'K')/z7׭VO-X 57)͝RIuP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@x^mg3ѭ5 ke门Z9cb9bJkyk2se{l]] լO:~VZ_ wg9i:+qg}gp%hAdu$R*Kw6?r|@}COMk?Gm~귬]$WF-$0X\@ xÞ9 jz5( 'VͶ##!dhdWt)n"P ( ( ( ( ( ( cGҼCj:Y6gq^oݍM ͭմM;$'#m{A3mOCڼ_x\M%Rt+i'lGNxnmLMg}g2͍GuguW$姆+kkK^HgX2@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( (^м]j6ev0ŝol2H%UhdTGPˋ8 5^:ko]k[ >ີq5ʗ67ͥqO")o ǿO/~"]j0eWL2]^xF[[3ϭ*CBxPִ[֭3Sӵ6X5M+Tu+ onY.`gIa'd2 ^ ( ( ( ( ( ( ( ( (?( ( ( ( ( ( ( _sBvgUkSiڶww HfD),2M ʈ7<?qlZi~i~귌]j am iv嵽@W|o#ŞkQjN]e8WX^i;K+d@: ( ( ( ( ( (2H&hghŕƝizXF\Z[ʭKee=r0Fh_X}7m~׬2"8PL-Z떶~W^txOW׼;YEi:/kwk.ppxnmHm.b(n!%ߠ ( ( ( ( ( (+\hAYk:&e>ZY_]Fb C$ȌAd2`'? Gu~9cď{, qiZʲYmmY-,#)n#@ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (8ψ#G:ρq[kmãq5e0[wXE~]?={xO߳I[\iw(R]t8cA/5u}Nl5{ -WJԴJN,g$k]%h6WF*( ( ( ( ( ( ( ( (?( ( ( ( ( ( ( GwYt=wúƟqe7ŭ|dpX&XH@?$ .+i/W{^0Pѓ' j9m ɵ`[?|--JG(q_zG-ݴ)9#p[̱G-PC,jiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPN>w:LZ·YOjVk͕緸G(1e8teuVP+M=_7_Dw7zR4I$hTi!E1ikVf^/wIg4<=ZG{jvE  Y Ex.gHm.b(gH۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴/ Oρsnxs\0^ZNE nA5e0[wID68ȅ|Ao~+ԧ-O{ોZi-.ze]wClr_ dm[J`Vze4B )-l,Yn!x]eh7WFe O\??q|G(۴nrO\??q|G(۴nrO\??q|G(۴ +y}oN@$uS5P@P@P@P@P@P( ( ( ( ( ( ( w?7jxwCnxkĺ]VsXj]+=pAE*2MI#弑 <Xm`yzuKaŨEɆV0f6մR>1tx_U n+/Tζ8el+$lw -QOP@P@P@P@P@P@ZGԼ=6X59SK K;;)4+)+˗լrD>1tx_U n+/TͶp~H)R{k巸)5d{wQBCi[6;K0#B>h0 ' IO]a0Ϸ|:gO]:ǨYien=K;.:rոv=RQ iFR{yXnB2tl:6Ua@P@P@P@P@PF(u?Lt=fm?T[K;),3DAʰ*Y#t_O>&O[s]~/"3Š#M)c~͵~x_xWV j k0yW|[̼_\IeK!+$2#:8ׂiKV ~*_xEx,xJY2iv+aka4%,'PԬ5:Oxm/-.cYnag{ytwX]@h ( (k ( ( ( ( ( (( ( ( ( ( ( ( w?7j _xwCvxkĺ]Vrj]Bk[Y9,IHH7PƍkVwc oֵ^* 'lC8 /4Mki;T3,w+(6KK}KJqO r"ʏ Ȓi@QZ]'*.,mn* 1Ld,^h又}RɊaHmm0$DkI:3l@P@P@P@P@*u?xLִ-fk SKgv ѷVHT'IQ@?-_O5m/ڭw5R킫!b;UXfc`[KZ'u$f$i^2[G0\r@ 񖱣z}V6[bH. | WDe)U%DGP?O%>a\^VstHv[O 4}cKch[uc}jdH8e?yYH)$NXeY!TtP *(׾kXh96nѡM*:4NGX?"j _n΁!˴j%][p[If+xJ|quuy#Mws=ԬrM$1gP@P@P@P@if_h͕VZXF% N ԬH$l*|B7XO%Z{ \dg q\XruQYCz%jVuc}jd7FS+ :ʯȒi@=$x.!E'Vc2K#c'XtB|Eu4v3B- I{$ni$ETFei,IbI>;Piw5Dmԯ, ~o Y9I!ڥT(}7Pl-5+7/m{wȠqHREq@c]Ej˥^)VbbIm$v);"8 /I;xl"`S5 : $DT.򮧭>]^H[J$hHvQ]MŴ[i淕H+$R:# ~!^_zss4ٺHԹ~;~LrZT(P@:hPoc#2JW昨#w7-r Ak?@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47?  Ak@,s)  ?f47? (b@麸6_jz~lzmm V%$@&9d //gp֢z pӑH0, 4ɶs8hѢX3O=Ùn&yM#OR]1'$" ( (4h-w^Ffs^YmNeX淸Ub2=46SDqAw7VEsmssA"ML$SC,e)c`Ȍ P@E48dE ;FGBH`ApT۾|E7rp]a;o就[J4؎9$/4s:)vCP@_%hP@P@P@P@P@P@( ( ( ( ( ( ( w?7j#<7ϖ7Sc^f> .E֬m-+R{[ A+ȅeUIbt='I-Gޠ:OHxÌִ?5QI,ݳր>Ԡ>*7F5N I>jv ( ( ( ( (3}#LYԴJKKĐ\A #pȌE":H񖕤jݖYk>6?mb;[+ XUxHAKgc e מ6*"A=2/ѣ4P@V&m$A{y3yd{uX>.s[ha?/wWK&69X#A->N6]KhbF奖ilV#ubSΙżlN{Db-c0 (|]'\5kz}m.uK‡ 2HDP@P@P@P@P@P@P@P@P@P@P@P@Pw[SmV 6 0HWy$G| + (5)^}GPBZInv'%Iv$KI'R ( (9x?;|Mb]agA/lkkg;9Su&s|F)喩[<;Kwh̳R0:I"X.^h ^ b2VHdIc`pU`ACA Ҁ 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j!ɦ=ׇ5Ա[^'O^.}>|0|@P@M1/id)1i&;j'7onFH}w@|g)l<]23jW7+ lFٶ2+/U!@9:( ( ( ( ( ( |QI44q<0TE(8@tgY,m"$t$qP7_j˼RpٷyY#,XWYRF;*4rFtt%]XVVYH1@ xv+]D-+eZ[a"qpXH*F!F`E唺5hc9eP2Y`!T I( }^ Ӽ(ג\*y@IaH^I€yC8#gkQowq4|[IB ƬfPaYݎ}v¢B4thzf;YZrk6eu!^wBJvS&j%O>lA5K+|gkP@tQ&i w`'8"CEb (R5+[I4[EYa0R9$ Ѫq"DtP@xO/m$UN r&-q}pOWIeMVIˋK\yԟ>/jufz^vxVݗ :0{baT6d( ( ( ( ( ( ( ( ( ( ( ( (@$$}_|3>qZkڕuUF[˥PFH ȜT#$q̌J@e.j3I- 7')pJKYQ#q@4P@P@P@P@]Ӭԯ׷P[ i7dPK1誥}@v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j VՅb[k$G*l`QR@xWeӮxd -a%^]~g19@FY"" %2IÏ?4jڟ-M?qg grS+yu= mK/C(խ$Y-k\3ŸFw7𛏅^5FH/ ]m.?މO=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS: peT{.ߎ+}J|/M -_ZF ig'iGUi(.W{k߈77MO\>0ᝀCڮb1߅Y I"jW ,M[m&Y#=HUTUDP*UF 01@ @ ߄R]MxfXgv].ἔF7ئшُRvl@x:ǿC?+K 6ӭe*sN D"UhX""DhG(TD@Q@UUPTcmadhdY"6d2G@iy7Sc]J30ݡ$]t9Q@p:v3ݺ%s  RFh ?O..܉!< n fVmxphκ/L`,70JW 쐬n{sN|)ߋz&i+"vbk60M#WuO ( ( ( ( ( ( ( ( ( ( ( ( ( (4ûoYb3,Mh1w^^dI#cEv-V#9DҢPHéiʍUXxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN "Yy+1dRY܈.р= ăSfX([ OFeG`3ziPP@P@P@P@P@P( ( ( ( ( ( ( w?7j1KjhvOm.0&k#RDt%({c#ɢIiR6xovJˁx !&@Ox'֋gzu!?\6tφ0ԝAO}JDT-tp?npO/f=X/u,a $aQ%L;Wy*P@P@P@P@P@P@P@P@P@P@P@P@P@P@|H|&MmegmMo]],{ltmBK+1-Ȋ߳ogOI,mu5ؼe|hm5{X.)ϟ?)eM ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( (>th >/V[o:\_kw Im4[9$ylrihA |7~>, k?u~>}6Y47eZD41yK<`tP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@_%hP@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( (h3?τռU}oA@_:]Ѭ&usmo(_V_;&ɦM6#i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘ|J>okxVY|o%5/jh7>"y,k''5Fx.lf|]?Q~0Z0=/:`̺\:i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?dDB(*$9@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (_KWuOj{_6uMOĚ*,g4>MoΡuee8߳W_xjڕ[T4% o-6s>3HWqhz$Ew}cWyUu#`tP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 7"Oxƥ HOkΑ%#EW呬&5}Yxx;Bٷpď'SSFڗ3߇|h~&j'mgHOjѠ>\,gY!WHl!8PComG?4mxQ0^9w{ǥJG3ZEǘ,,O;C'.3IukhP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (h~>~)_j{O xJtU>) m4C$R<}wW667`$~_-x/ګ{%4{۽&ZIh$=q$%ψ~P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (?ho'z؞ž8ojamld6}ib{}:lu<LJ7kb}FԯյUhvWdVB7+*Akhg-tB׵wt++txfQIZ uePv (>`)g~̿W? ~ Vqg-NiR7:mi_]b}.Vz$J2]}?@P@P@PП'g?xi|-kYWF[K@OX][V'mI[ln>9gmWMnn7ȚO4sq_- 8t/r uծ"> ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (?j7t:ğ >Zvq3[[k奣E"?yϧX\MiGUT` i_*3~?y?K .Sڗc\]CY &x'>3þeaxiHno#GğGi%vRo.=_:gb׵o^Z˩^%uMrS~Z|9kz i:n;KL_>-~>&σiz (M iͨ-a_o׺&>YHqOxWM?b߳f͡x\3ϣND#O _MhvJW|Au i,-5KoD~~jᆡC~ּ/]7}|ui :&i Bh~|8O?şv?$>U< i}JӤZAQNwM[uϏ%hg~#!u O}׵>>񗋢n[ގNml̫eh#Mu; R?{~>0=IpA֬5\xx0.fEO,~؟ׅ~ȇ'ħľ+}{Low[xFBc2jנh=િࠧ^<ɬg$@ >_k]+=JRkM#WK Yg}p2yOX|Q>i^ cѴlOI^xͺ.ct Wt6` _Gß?1nP (1|1YM_:G,lHw.i̮=vw@~;Ѩ\y7 >]"iCsjMZ>r\\kO}x?ukm> Wh:uĺF;mP:431}_UC[>oai >L{>bMbO2OktˬZZxZf6\klQ~zf [w'v7칥f_ /sxC\TWɨڿmt[]hi$!}a8X.P,WbYye #2ij@Q~Q~#X[P|kFCJ]r?[]Ҝ-Aw<3-Cy{-ø.m}_T(nPg,]^_fͼAJ߻TY%r }3K?hxi,S*E7 2k:?'4Jf\l,;ɧX0 (Sմ/CҴM>3g7σ*i?k,$?49Z/t 5 CqkW^_Y>gc@>0khW7?G4it|?7~.u5KD5]SyOt[MO{NQ #%_> Cc 7_$?uOϷfkja+/7}kx.m]g?־ xol/4gھ%:<6wx~]OJe֧:Je/.?˃Ev}wž+Xxƣǖw~4|Cdeյu+Z[8llLҭ:/wӞE5_ƶ~tMM4/t+f=b -N.-=NG>K>*EC.'dV<(F⟊3A_xf2ZYwW-nAOGXXZjo /M x^6z6KĺT 6:% . 5[KUԼG꺜$wV6oqg8;&(n/Nu_Vgĝ/Lcxg3#?_ھ4+RkHe _Go*L%SϴE Ÿo򿰵Onn|߱@P@'߁gƗ{6þ1xXMӣ}#L"]2o>"?Ojڒ̎O*7›>iҵ-CKϗGn:ZckH䶷I ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (<_?|}_~]ɩG>#Þ2}5bںh隁Sm/Mk1 ahfE!+x !R8BGh*""EPT ( ( (ž<|w7Kx'cǖ^/LK鶻'$῀vP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (# zf{* bK.>ٮêWw:"tht  xW3GGP t7Q1~xM_G?Su1@3GGP t7Q1~xM_G?Su1@3GGP t7Qn/JOVoSxJ(kx/r.5h/d\YͲ=.TQ%Y]~P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (icingadb-web-1.4.0/doc/res/ListItemAnatomyDetailed.jpg000066400000000000000000002002441516124654200226160ustar00rootroot00000000000000JFIFHH@ExifMM*i'C8Photoshop 3.08BIM8BIM%ُ B~C' }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzCC ?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( 5M0}j6y^`)0˼J݁1@ ^j'eiz.j71Zie!x3ZYIgAxúKkeW}E5q\41Ko:4nP@PsM O=ı ctM"EkʢG,@;[KwVp(e+èLRW9=EX ( ( ( ( ( ( (..m3Ak 5ı;T$d@X8h;? IL8u.Y%cPjl"LXU,p8PI9?aҵ w/į|N4^@5.b*WGCx?[U<:j.lF-o6U;'N<ҀN_%| :~^au?wZx >j͕wo@>Gd3|\j3i{k4ú74hHyo@1vYG0h@xn?jߵWƶ2 ]#RVҵk IP:7vyrַ]uk{8_{/7(ր ( ( ( ( ( ( (>Id~۟ωgZ>}_ڼ",luNO kZ1Z,r-#vd(4~ ⯉Y> _G4^tuoG'iʹB2kmO~)7ōkៀ [xWwVZ'E⛽XL3XX][Ck5RA3j1(+*SZ?wl*yx|Y_}W4ɪ[O ֙ckzPX4Wnw2~_>}-t?62jĝ1ҠIwhjރcAk Xl+ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (>t?u9mҵ1$x'g &Pnm{=<ʠ(gJoٮnZ[IO|%5ޝ6 5☵u\|>ȱ zΑo5M;Ct+Śηn_ږj7CiaiPOw{{u,VֶK<$H@?8o /F'4; yχuǂZ=լ _|KXHO7Qo$2~1~m/w\h67:4ѪxRCb-"Úև}:n.-jZ|gP/ ='Moo&|3K{/ NDwSw\kxfk hqrFR(&xC%?-Xx;CE4}e>|7c?lk85t?hP4 +]g6Z.v3XnPC;~?a_>&67Z>+rzⰵRڎgo-H泊{PԚ( ( ( ( ( ( ( ( ( ( (?( 9gO]?~ JW?g??KAO\ou f6bw~'oihpGyOt8֭Ui5/ʑy&y@?i?Z|B0٧N;[0xXx&Lzw !nc߈?_:v/i/n1A?5-MVH -^J4,S?(K cfG'^^s=^ T8[\ꊐWB/Yf[Nԯ/RS^2|os/A>Ӻxźx"Ÿ5oV5wג̚}45ksVGo/5-=7{jvLJ{6+ٞXi{o=+eJO?P_~ҫ\| 1U|GZ5։蚌Z5 f>Vqjv uwٻF/%4}koϋ,mjMs+i|'Ah[0{w|V[,"77 QFS~@P@P@P@P@P@P@P@O#ggb/9|,gO7O괿3 燼YS%.cY}K4<}sn~ŤoqSB)X+P^,~W^$|Cs *t sDui=A|I HM6dn B+*SZ><|aO SX|ic(S٤x↡wᏉ0ކ 4T|S6j^6K[8`tWট_ ~&ŸWe%֠PR~"O}%x.onn ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (>tG؛ o7n=5[I;I|3_ Zlp2O>$^"f,㕡VN7`3_{f{7'û8'c J_@HMo+|~+w?Yl 3׺|o-5o|" T"Oi^ BWpַ}u~>} E?/uψ)WjmuOXjJ*x-.tKMl C}8jٻF~~_LJ>Nj|M_H=>'O>Pχ4 %@^,~W^$|Cs *t sDui=A|I HM6dn BCџJL>3<;sqŪBm$K[tg{ ( ( ( ( ( ( ( ( ( ( (( |L'>jE؝|Q^h̬ϧ\F +X8?? ~'|f _F桢S/YMgv-ûuiO\xȏGpͪ46`/ſ7i>/|9^.<7i|x%4m#^ZJ{@ Ysǐ6Ե-YumλU߈nnO;V?#Z{'Ej6k]&Q-4ydTF-~ԟe_(xڳ? <+Q׊~"^xwF<xUwzRӬ#F|)xoͽ y~)~K⿋x0P@P@P@P@P@P@P@K`|5?.Ax>i_|Bu^M id7ϧ4M-K~0.(ggqEk 5]MrDz  ⴞ4伝eh&[`ΖJ,_mqoyxg΋? OuG]>~.֭-PMaWIo4kfQj "Gpgݖ5߁_Fs9[i~!hSxBn|Mx_95Ww jڵz6CR{'^ ? u5[  iu?Mr_,^1"%SnG7<'Xk|z^>YԼ{C=nu]|m"? xD״kVj?,kz~ π<;KĆ}N׾#׼9q}>X[~:mr[ ľK<]r\O{siwū\zL'ĺP_V?|gp:  ZPEF&}2棨jut_Fs9[i~!hSxBn|Mx_95Ww jڵz6CR{'^/#{~|4/V?C7?d׼q!x_~ ]fQt#:eΏ(9?m?ڟWgoƭs~6-㯈q{ k'Ě>35 i wGֳᨵǭjZgu ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( <? ICպg>Oy?n@m#Q=G6T_(?GQH/z?iE@m#Q=G6T_(?GQH/z?iE@m#Q{IӴ@Y$M4t[B4fy`YcΌ+,aeeڛßO.m/-<*e߁.{;y~>vV]ka;fԀ> ( ( ( ( (¿h?_im =%f[+Zo[}.6T_(?GQH/z?iE@m#Q=G6T_(?9vgtOz|YuXtA`m{`7VfeRBUǥ]XOY\P釀y?Ѽof^޻jVk[[Y#H'XP_@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@ F,?,%?ں|~_?؞^>O?8P?GQH/z?iE@m#Q=G6T_(?GQ>8||:mD ( ( ( ( ( ( ((j^i6ai:ƟiWv77q476v$oq R"2:1VR + m[_5h3hK_]HEuk,DOdEgtU!ωfK5?R |Ko6U V8#%&57+՝RF@uP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@yw}O^<~u;#4-V4uִ[Cgٙkmh.`{+خlnnmCwl?g3PR[| 2J4-;HK^CmaK\^ӯ?W9cIbt)QdH ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( en_<,:pZAm3[[IM ڵl隐3{gớkd/ /|w[6 +$$.f#5zٚ]5 ( ( ( ( ( ( ( [I53Pu>V5[;?S5 xonhnm.I"x)ш '<{q[j3|iC D]P k1h-nkēǨJoWOto+Y׼7Z-ޝ6UDf^"[kh ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (,s Ǻ`/Gc}]U$KMkEh6%^汾Pwxn>2|^d]6MV^s4} ͽs໩"in'uj6*$teʲpA4( ( ( ( ( ( ( ( ( (( ( ( ( ( ( (Iw;?p |th:v" kvD]5]569={Z|7Exn'5ڕ\[I;K 'UDcþ!V\K^5_ h3fixk_K3Src6%$Xlַ$׶W)ݕSZCIu4P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@ x㿁/NV`ϦV4ռ=G:?ٯe汽ypϯl i}.^rUтmum*myer]]50B(M@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@Gু=xP'Z\nҵ[u5j]: e[_[iv Ht6ڇx͍hfO¿e5+h8L{z\wR--ҴxZIoHvܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPV6{:LZ·\iڮsecu{k%fI#8e*Y@?(nOSk\b3h29>j$I+$w H/xmU;M ե[yh/k+heAۆ&%[{/Th7k]4zexz]:OJʳ@  $ 94vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#o:\“ ` )qҀ& ( ( ( ( ( ( ( ( ( ( ( ( w?7j_ch#x9Ìu [JGkXʵ}n{Uk{K־%civ>8~?h/ ̭xs7mm૆x.K\E?ޫ]iWM~Z߀}9@P@P@P@P@P@P@P@ek]R4=cD,M/P.lP52l:@?,O; 2z{(h ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j?i٣ćĐ?όv5m#NXikSkn{Pvwm|MjMpG/ s+B[x*;>74.gn.A7\wMaoqv &8^WTAf d?9m$mm}V>3?LPm[J!l=>잂r~) ( ( ( ( ( ( t-6zƉYͧ^ \Y!I`' r8FUY@?-/,sZj4m'?jwld;#iu#Ei{(SMdYP/ x6 j֚燵HﴽRO2L$3"VW61ms WIo@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@Pj-௎ _Ftmf#5a$;"geͳ_-nQq/|m4xj7.V?nO쫽+n}pN(f;[yf7>+n3X~#_@P@PaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7j>fI;LmCXH|{kbCkn5µI>Lz/<5]dѷ#&/4yqΌ ˥\RE[]lk5E-!SE9PɲE闞D$`i$d'|`1$.eW?=pһ`@u:G|O2W0?nڕX B"F^(ۼ/sMԚ;=z$nj-363>vcYzy, d+{RXVR A8#Z( ( ( ( ( ( @|S~gz͜~³^&a&Ul:2Ȉksm'ώPXma~e5?l]ʤwF,ScX޼ͭ~_xWV:|'I~6.}?ujZŕqUn{FO#X2(X3FI<](ĞOi:7i.&M`P}wC5P@P%χKdC#6%ug;FeoQE,sGкK$l$E VGRXA> ֵk] KoIqWrBE y*<ɥt<‚hx^%Ē]^McҼvǟYo$4`T{R9*( ( ( ( ( [Iu6G,/Q1,6u+ZU_[ZVnV7~:0h@2I ȒjP@V蓥ƗZ:v$`2$lw[t2$:}B2v,wGkۏuWkvc7F@%U}nEhw9Exw/XnӴUo f1*%( 2# KJP׫CHf,7-\LHiG`2@*{3ĺzj:d--Ŵ\F2 u!fR6'9${÷c}3@ln$y\9T$d b1ʹd4_iz+2!xIr:Rb(WPumZDRԧ[{[p 9(|K#|O' j_4}";`p4 o$N aqVYK[x fU'2^EA A #Gc&º4ڔ&[=܊"Q嘌- gĺ޿;ϩbn$h!8HmQi 6Z0HC$H*# X~ g{PD׮Dm6wBBĴҹ1g_XU@> ( ( ( ( ( ( ( ( ( ( ( ( ($|D Zc:,NGkm d,&Fbs8<eYbKI4+=Iw,Ğ䓚 ( ( _ |7Kw58U~ߣ+휍dcØqIİ|?Vxf].?~X-"|i<֠iP@E48dE ;FGBH`ApT۾|E7rp]a;o就[J4؎9$/4s:)vCP@_%hP@P@P@P@P@P@( ( ( ( ( ( ( w?7j cU4VBҰ4Bɥd.ֵ{S/|R C)Asvf S7eu/|!CGNUX` ( ( ( ( ( _H52Flm-/R1$H0pA dEI#eU_ɾ(#':|=*]ƫ1kl+Q>ѵ/:]_[ZVnV7ֲ Nh ʉ,n@{WI\k .M6)]sxn#$w*]N}OڹcpmdbLvN&ەHP"$Ymx]XCX1=O=OVoַK~_6/!ߍb59s@ExW~ 2VHMVT`pU㰝90(*(>++I)= 4j}OJm(<=۩al]ͩxB\3I&&|4? HQ>ߣ_ڗʜcØ/_Iİ|>-|y}`_ʳٶ [&چ!KPKђC"KRbPޔPaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7jѭ3w]6:s##jP@|gsk.K;[RiL q=`>K25[EH ՗>imh>lyIh'-03҈ pƀ,@wN/5V)qeqGh6p H#866z]E;Uр`v`Ahy)n"eK ?R($6l +]1|@P@P@P@P@P@f>.coiZ7Q A#pȌE":HŸ9OkI}Go6igf+m%헜Y,/$CQE}5@{;)M淩"안8yeuSИ.3u} @+|TƕE"V;̓V7iEk .0 MHu+].3%́s1gރj>ڱ6VgoBڂ>`e-u>VggedfGVVV*VS`AF0hP@N|wwrTx3p 0fXLPGO\e+XULVkmp@If9[i"p@>zHD,qy$w!Qgf!UTIPd4˻7-ő0D"Vh€z ik5յ..%hyf,Pݱ 8"4DAq@ ( ( ( ( ( ( ( ( ( ( ( ( w^Mֵ[ԤQAc8%%ոȠ ( ( ( 󿉿 7Ss{0mu6BokRɼrmuh9ς? s&ˣa{iР]};0 ӡ}Z ږ@t)u+->-5'EM!Tz*c3@u@_%hP@P@P@P@P@P@( ( ( ( ( ( ( w?7j$^-+(PioN2>8z( ( (Oy=]=vDcyb_3DCu$3C"XRFxeu%Xw>wJ'^YK+v?ivqG0g`K=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN }+/'A ]"%7!I#wĽp>д;iizte ,O;UUH,h@RRY 9)#WFT?2!mP 7v#ƀ;] :'h̀ bO%A$@HI(ޠ[Şn,٠K+4eNJܻ©VI>|M YY-mmu(vMm{m+7%Y@uv ?ʞjG.%T Pxze-ܧ"o#p3V>cz__2ܫobC]W @/xĻ_T r/mݠU™ST\βJMr#i0Eԯ°a y9#" WuGvV6A"=.!;{hQcQc(aR8 K@1_cXV&጗Wy3Hrң!YZ&/˛w2.>=°utb95Ӭс yhІe2HBܠEZ[XZeg AkkC1,q¨I8噉fbY3@2#:d`YXa 0G94w`e/È<1  2DbV-@,cl @v_xAmt@P@P@P@P@P@( ( ( ( ( ( ( w?7j|]n?mo;ڀ9( ( (;O۽MٷtZ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j$Y5++@wo3J>8z( ( (OMg=vwdxo0P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@PaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7jѢ3 3!闬]=:s;rP@P@P:SӭTVp/#c X@pXYCXYZkzXV!Ac݉' tP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@PaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7j cJ*w[1mLeT.ִ{Sd4GɇCgkue ( (=7x"H4tq2vUz( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (K?4( ( ( ( ( (?( ( ( ( ( ( ( w?7j8_x&ŶAhk.KK[ĕ` H9@>OԴ"k F[Kta9c-(( (3Ejo[trO:2Zbk1@w 8cQ"$H5DE\*(  b@ekzŞE{ $PIes1TWuS˻{}*qᴶeOfs$̋ ȉAg? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  ??f4? sAh@,s)?  C?f4^ w}[#1`Ԣ@VC8LBVg"4%rՑH= 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j AU2.";bL3m$pZ' Ex_ә5A -^x$.b0HHA"U >O&^uuo- *!?N@P PSx+/ش c* _ < &WV=5ץMZv"иVeBA۾(*UU@UU*8 3G|Zjvw7ArѼ|Un.- cwI( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (msyekf\]A 211; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ()4q꒢ !3[t$lsVɀ9-i:U git6v1FEhP@P@O70oqM3E"HRH G4:m25{6bkQ,y76EYgŽ(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@(?j?GOP ;ڀQ4Ž(wMDh'}SGQ?@߄_ %u0wp[-n3*H˴IՀ= 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( (9_xc@VKkcǻw_";9u*[r|pZʼnBU31FE$DQQPuP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@oZikuw5Ƌq4p\O#H#6ٌ&Cec5ee.#x>; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ( ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( 4(B ( ( ( ( ( (( ( ( ( ( ( ( w?7j ( ( ("xmT#yF qF݉UTI<^hu?:eŦi(ECOY1qFm1eX\JRV@/ޚ?xԯ ^?+o4_+J[MWzhRV@/ޚ?xԯ ^?+o4_+J[MWzhRV@/ޚ?xԯ ^?+o4_+J[MWzhRV@/ޚ?xԯ ^?+o4_+J[MWzhRV@/ޚ?xԯ ^?+o4_+J[MWzhRV@/ޚ?xԯ ^?+o4_+J[MWzhRV@/ޚ?xԯ ^?+o4_+J[MWzhRV@/ޚ?xԯ ^?+o4_+J[MWzhm3 vU-.dKEeMVE9)v(@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@_%hP@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( ( (<`z\o. GEk{u99(( ( ( ( w{g۽ݵF1%[[e!<2FI(0/#keV@P@P@P@P@P@P@P@P@P@P@P@sO,.fwyʍ`w@vP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@_%hP@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( ( (2^&=HR)'$2*ȹʒX23|ש$eaoWZ'L}FJf3XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=aOG?XxS:V9TN=^׼%%-sjhY@yds}IEaf#gD`t|KC$.h<+itV161,#EgV>sOv1ZXAgkLv$0eQi FI]|nj ?ʞ°u<i^gxCKc-9y$Mqq+Iquq$72qi%Y ss;sws,72q,P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@PaDŽGI@P@P@P@P@P@P@( ( ( ( ( ( ( w?7j ( ( ( ( ( ( (/[ hVz>ϨjogcglI.&"﹎;*࠾i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?/[<;Rs&eq꺦ml,mc2\JQEfcY`ɝNQORwS7]b?~;oKGYcd]ʅ&Ӵ[O Y>qJxG~!M;Yciv"3$VYnni%Fi>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@-?%[ߘdk얟o~ct}}nZϵKO1>i>?7@"$jQFpTd$I4( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( u'úNkڍwjX\]]\JPwvܐ(_[|{?'>;_Fz-Nh෎d%̪R}+L$jB~x3{ 4 hievHV("R]ش7w3<7/-ܳ]]K-;tP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P( ( ( ( ( ( ( ( ( ( ( ( ( (3}_KgMơzV6647ww32E ČI#*O~J࢟.<ˍ_ux)@K=Cf',-~._< OѼBރj~jd739i/&gy.o.GFf ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( jZ~jڵ閗qM=[,Jqwe4)x  O~˾  -o|?>moBVV/#}{^ᤸwW%P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P( ( ( ( ( ( ( ( 1I$9$O|[_M{ox^L(4]gN&RC{i*G:@ g( (<~.)о ./:G/iυ~/,?fSV,[j&h7vZ-R+-sKՎi[ygջ[P4~ _uޱ3Yo־!Դ{ J|7kwZ˯X>?.׉RլOxRRQ&gO!W ^)a>tKZ0.iW~PDdNԢlg_.׉9rd?SkZ|+ KM-⻱h-!whdI#vF @ ( zmޣXi_^M5[,QH쪬hW~9Wf j0K1 D$3=hRڞ# qEۀ~|8qѼ ] ú$U_<3QnHjw{K<@;( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (*WAoز៌>n5{}SYw)滣E*:dd@zV"8׀|0x{_=}oj R[)6v%}pi^kZukcl<~C~P'7cV/|*oi^5iEæ|C)_& ֲҭ5/o[Z\O x5?HMxtoLI_y?& #C l_?Eľ$G4zuT{K˟G=JiM[9K&`c! ZOu2I43g5} z~ G<mLJ5 iZNKgmjķixc 8Oį5~#~ECU:_SM^Eu]rLuۿ֚N-[RҴ/i6ztd5P>>l~?l/KoX|usm _K'WGXmt{k9.=Fpi69_:C6't>%I,>ji/W:rM}!@3n$V_@AK}]7ĭ>P->ԯ9yZOu,hzƓjwlI/t }M@P{[ [{;+;y/.6fX$yfVXY݂*/oo+}{7E6 !76S,t 7eOhAZ/~ C~"?C8]]_꺭ՒU|˹yvK VwP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( (ƾ?UO)'?~ş [.6AOo]_zZYop%X]W77xZ[4){rOn:]KV.~ӫjw*7Ҭ /n"(Ph ( ( KKH%8-yYbbFYde8՝*>#x߁~G cl|sa;z t\?Ti|^/1~Y/uVծ# @38- ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (<+f &jYoE?j<|Gq?iE@m#Q=G6T_(?GQH/z?iE@m#QW(?K;??oY>թE1 [ FdI@?G~-Wi1;d/Tf]GoW.,V[@tP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (icingadb-web-1.4.0/doc/res/ListItemDowntimeAnatomy.jpg000066400000000000000000001747471516124654200227130ustar00rootroot00000000000000JFIFHH@ExifMM*i'8Photoshop 3.08BIM8BIM%ُ B~' }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzCC ?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( uk}D5ۨEҵ Z+pq-i5IfgXĒƅʆPw"e~ӿ U^2)NW=z|__Z~>=߇>:jwϬxR-[bbҼ?|] OTŊ..jLƟL!O=Ρ|Yc腅ђwkp.2| Pw_>gƙ.Oa:: Gr9M(vpĸ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( g>>~?gŏ |@vO't/~7е|]^Oj,qx.DȀ]c+>XtIk-+A~ AZÚ}V4үt~캥}'Yɹi_%rĚ(|sM߉Cx촛}oXд6:FksӴx|./Wo/+?Yj;I/,߈KfsYy 70]+ZK}@|_?hi4=K{Zx'6z-5ڳx泺qx%;~P( ( ( ( ( ( ( ( ( ( ( (( sk/*;#L2xÿk⟄-*_ ![ןZ>:m <%x>#i<'k1qv_fCI-E<O(?w g6Goh?? k,:ƿjz7w/E5 w' Mk ʺ,R-λ/-SK4('] 7s,zx7^Rĺ.-n;ir4]Tw@?S⏁'ſ< h/j`4zʱ x⸷E4NP@P@@@ ( ( ( ( ( ( ( ( ( (>tǾ'lĻOoiCCix|E%!whRo|(%E5ǓH [k߱k&6|#]A沱ÝK⇈Ԗ7H$}ᯆ+Y?{ycuoe_>xdGv>\A'(+OLycw:gM_:CZymUq Pޏk?+\7tXc?i )é-Z]ƾ4ЯbM3izX\軡柀_R(7y|<ࢋo+_Zo/cEމB}&е]W$l_ XVEuqմTFD@P@P@P@P@P@P@P@P@P@P@( a?E-1U/#,mv+Eo!ko~:oï~$NY./~5^͸I⏇W!D1:3H'w߲1UnOIvE園j1j_u ]xd4/4vgYVXK4(C|_m;ڳUg!ux ^ѼzI54ڐ|1mA,L'^sg K>j 1>xOwŸ)e¿(݆$\!𝷈bkKuƧoI&A_F kYtM4,%쁤_4?|vҾ93Ě'to|Wc[:G Oh>!m|Bg5=I:fڅj >uF'Pcg1?ڼ?u~bύ&?lZn_Wv)zaia麬jWvu#{c¿u?n+K^7ƫ?:&Y']/X,]c{s%֗74=+ÑcK|.c_'$o'ſk>wk6^w+i+q_>}w^BIӠnt.kYY~_{/7(ր ( ( ( ( ( ( s0:sV;Q[Ư?k]#,Ki[KHw,;:( ( ( ( ( ( ( ( ( ( !1nm81^< ]]Gut5_ٞ.Gu=buk bV7gV= I7?n_ fb0h:!nm<7>𶍮i^+ueCCi:m ]4-x%'k ?O |xJwSЦ"O \:,B,ux M OK'+_>?:^.4 cWF!ӴOrxJDKJo@>y?j~k~)\\Q-νiZ|=gRԆhM>Κ^K#+gO_Jk_Ɵ #>%Ks\MAZSG4b5=c[м+3u:t뻫y@>ECjZx>?\G?uK|Uuͥ~ ռ1IT/|Gh5BU@?(LMT|Xs)ʮRJgcI2Ή>7)y&%F$~AD@?g//Zy/GizjO¾O =̰K/7*Cis?b/N߈O>#|[|| iշO__iO ]zF}S~ koX.,^?hψ?ηXk7%mO~i֟OvQm5ƦMBBn?hu<7/ cғ~,Ҽ_Ze^nu ZXib)@/_ګ/;5|ze sG^njOPy!GյI-Xs:=6So>^3/\d !Ԟо=vZψ^0캟ƭ*W':Mּ#[ϧEmuduזRE? ( ( ( ( ( ( ( ( ( ( (( iQʟ\EZ䠂bGQ" 𝟉t h{ t|J; ɮlfkTF?ho&O9gاƟoV}IAmXů3h5tG,u?5\|ku垡x¿um3z<:!E|8a+!iO'|7|W⯍^{\5}6υc:ޓBZ ON|9KK}6%ԼUjVoƟF)guω,zW $4_|Bwyk.M\k]KZyۭy7g|~NI.w^u kkFᆅ(W$> 4ۭcRՠIH?,?z@?n^Ê ( ( ( ( ( (Em$~f:/ῇß'7V[ե~knv]js$6J~dҚ{υߴN{j?N%]L7"W04nҧ˶1[n Y-l cK'?@]54m;k>."KymΈ ;~eoSSu:^檱jzvmeܛkG_'W;w~,uOxž:xvh,^%ִi0uju]E͖ @?M ( ( ( ( ( ( ( ( ( (? ?ß ;D :[1+ -3'73u C,5?~+ۭ^چ}YfC,_Iwqij|.o_O>=?GoDggw<|'|xsZimM~1l9tdv_>o$C ZK?__/]ſ4xK. |cx8Լ_xC_{^̚|֚F j>IK+'+wZo{}[@C ږg^ŗvŷS IRM6 /ۿ-~֔77^tOƳৎ.o*棣ii]f|q>i>3ӮYuH$iv:Tj? |;~=x+AֶZ{a%ť>mYgCӴH% :0?g+- |$iOZ׏`kA]X4;OMq7~#jI$Y%RPOڂ?>-_gϊ׌IZt #}GB^> 7|Iּ-,o|S@?o4_x#G5>֛pFK \Al1,3pMuP@P@P@P@P@P@P@P@P@P͟;< G=o x|Yxa:%ƣs;S-őm xk#KD7 mOxgok>$Aɣ{HCaZ}HU]M\YG^]uxPH4MO-4Z .e"ӡ?AG b+RNm;z%/|H m#o^*]3‹j|99׆]xc_񞳪xRԼOk:ogUJP@P@P@P@P@P@P@P@P@P@P@( ( ]FMM4/!eg}m$w 76?eß4~!Ϯk:.mq{=>Zmf/SZ\Z<6@/?i?lk %}:ۉ5k x jVvT?|E}kcm[ &O3ǰV%ЭkiV77Zfgy%.lo(xbIn@3d7hPeiROf5urMͺ(L7]i`  A 8 4>RL+ZحIiD&%sI'P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( sſP^*7uO~b;o \[&W:V 0Y[Y)*plP=3-O#?o4>3-O#?o4>3-O#?o4>3-O#?o4>3-O#?o4>3-O#?o4>3-O#?o4>3-O#?o4>3-O#?o4>NO?:^-ߎ7Ş~;+U̷ $3"ݥCugu֗PCs (E@P@P@P@P?i~ 𯉼e<mbKhMizson4󭥬*A  hyAow~yAow~yAow~yAow~yAow~$ a-I,QIi|BLRK/%ʊI67mv֑=#M״JXu;}GK4onYmn"fXe\Ei@P@P@P@P@P@P@P@P@P<[?e x?@<[?e x?@<[?e x?@<[?e x?@<[?e x?@<[?e x?@<[?xC 1ĺ?Qi7z}?shϑm$\M;.&?>hb-"}@P@P@P@P@P@P@P@P@P@P˿l?E ~ֱ: 5-[J)QRsw_+BFU'@@=x{ݯQ]yF&7;dm#^Cmڥ< -2i7H|?y~U~@P@P@P@P@P@P@P@P@P@s)\Wq¶j6l,nM*’4Eyepˀe·wBVB_  ^ [C?[; Kh@+!x/ mho/- Ѕ%?Aÿ4·wBVB_ ߴ'1~eya/Gi.W+^ECë -McZm2k{(j3Η}O|k` [ _Z5 f.钆/Z]m ( ( ( ( ( ( ( ( ( (( ( ( ( ( (1CŚvZރOjV\Y_Y\I`'G̬6r*W~?r|@}SKMk?GUaH#5Xnx,Wciڄ嵕~OŞׇ4xGX׼9YjyVdw$3"VWVwQMku W0tTP@P@P@P@P@P@P+x[7F׼5=))") kyi2sc}m$WVWqEum,sĎ}|kqR7"*>7nmsLsÐGEӤj^懨>goiqݍK5ݥ,O:++dz(P@P@P@P@P@P@P@P@P@P@P@P@x_?x%׃|ao{<ۿ xWK&b}cU-;U 6w~xS. j`Y6iIFm#NnB}Dn rq`ԭg@ ( ( ( ( ( ( ( ( ( ( ( (> eMWڥ_WߴLzޟ$V1x v&B5򵎩b}.[k,j+ޓ{|ih[agwh֗nŏǓio/Y^[h ( ( ( ( ( ( ( ( (?( ( ( ( ( ( ?hzKYkze>iZ qg}ep%x;ԬH,N:~MZx Դּs$xZ_n{Wޫxʋ$l*ms!Ŗ0G[kvW?xs>]b^eXI]IC42+uk:EuguַQCs (C@P@P@P@P@P@P@r~97~$xWYWtk]{ZZj:uڒx%BZ^Lsc}m$WVWQEsm,sD>- 㫍[_uyiKj<]G:<պM_ Nդfi֋YFgoiqݍK5ݥ/$SK:0 P@P@P@P@P@P@P@P@P@P@P@P@G7\"T'hMW\O1"[VȘ/)Գ/,|HI4i2;[7ZO،[Zˋ?%j ( ( ( ( ( ( ( ( ( ( ( ( #uo_~7?hzƟ$V0 |d cgw|cX;x_Ytmn4S= ( ( ( ( ( ( ( ( (?( ( ( ( ( ( (^[6eݔv0ŝ`AalH,N:~M[x  =5:֗[UwްEtg`_Ċoq!Ŗ0%嵝~xS~χ4KW׼9YEvdd,M ]ZΑ\50E$J@P@P@P@P@P@P@ O~|m^޽j֚tiYY̩sc}l]YEżJix o3զ7F.w#-V|"ִh!ՠYMR5p֭+Uu6Yѯ5M'U4J++{kKYx]dXݑс(P@P@P@P@P@P@P@P@P@P@P@P@|F~ /M߿~^9,/( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( oxwBngUkvSڶww XfGR*кJ7qo'u~:֗yk[ޭzќ"U @Wi |UxwHo{-{ÚZiv2HEx.Hm.b(n!%( ( ( ( ( ( (CǞ >ko]k[ >ີq5ʗ67͝Q")o xǿO"]j1|e<C W(M[`DRe?[ִI?ZONִ cG4RMԬ."g{]%hD`H4z ( ( ( ( ( ( ( ( ( ( ( (>uf> WK&'omWԼ7:hZ8մ##-lm>_ixƃVٴCCa/ϧxLb6ӬK{^GZkIRvU@P@P@P@P@P@P@P@P@P@P@P@P@|U׉?o?ɨ+i ;67@,\6a`Z[v5{'/Wh-D,?| g]w qi7æ5/lnҤP@P@P@P@P@P@P@P@P@( ( ( ( ( ( (1|G iVZ恮YMjN YY.!69ʩ4.:~N\A']k?G@y;o*3HUHfs l KK-]𯊼;-exs^Pum>_6\!dX^ i;K嵺+d@: ( ( ( ( ( ( (|%?Zρo[kmZT.'\MgYLsc}n\ȎDŽW'~Iab^vеmq`QzޏZ//xEZ^Y."w:ƑkzEiOx<9~WԀ> ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( oxoXht=KwF-CMIm^$2DZ+hHTg}kvtGeojt6kn3X^iuUdVcm 8?i߆#Ҭu=Ru;tҞ~'!уG4R*M RjA3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4@{⏄ušv\XaoygpЖP%ō%ŭi,L~y'3ύ'fj7/[[6[w%]Z<719xA tkO7W6 +[k}{{yI MrF쎌I,A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/4A3'-/479|4CO&} e?/Ă֒{XAʗM yl wiča-'ƚr5.n}eU6ZO"X{ ?i ?i ?i ?i ?i ?i ?i ?i ?i_Mڤ0Cy%6eyٕ`CK 8( @ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@G =\Gׄu R[Zx/.KKi>&a#{~D֒H^տfß4 3W¿<, Xnm:o{ }*Kb5|ٺW_vܸ#mrPiˏ?9@ݧ.?vܸ#mrPiˏ?9@ݧ.?vܸ#mrPs L7̧ :OJ ( ( ( ( ( (( ( ( ( ( ( ( 5ox/wvOFO7z_=!j2OvhIgUQ50U5Tap#%Ē{EP@P@M^_nҨh ( ( ( ( ( ( ( ( ( (sd׵mv 0.-%n>&x,FZ?fOkBjᏊ^Sk --mov~L`=ۯt;ԋ ( (; /< :JР ( ( ( ( ( ( ( ( ( ( ( ( (S-h'ӿx[wO еN)ob9o|!o:O G-bm-n܌yė7bkKwYaAd`o j94~Ϟ.e>w[^xNy{0DHoH.!D6~X^ 5-7Fm\Z\ij[]Z=K ;,nTE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?O? Z 1@"k@>(S-h'ӿx[wO еN)obE<-Bց};PZ?OӲ4ؚ :[CkJʈҴp"!hUI!@P P@P@P@P@P@P@P@P@P@P@P˯Z?ܿ~7x]wip[-<.>..լCdJ3[D?/3|~ťxgw6MϛͥͼdUu&-1M4[ԲP ( (K?4( ( ( ( ( (?( ( ( ( ( ( ( tɸw]#uQYҼCXz%jVuc}j:0<2c)eUxfDN ( ( ( ( ȯhn ( ( (|%'L:LJY\k ZC*,@^ a/|*wxwE>0<yLZݤA0d2ka<>j|Wz]_^XYͥk5խ,Oo<.4N+2hP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@|M~!>?5a/t.mnK@ֲtg+&:n eoٛosn7sAsgs }SK]k1#*F]ze8&? ֖DdDZ?gL?YS5A9cho3X?gL?YS5A9ch{c⟄uoxhuX˞ 4]'2m:iK-JR&AN>@.e>kҷ|z,V[ v["ͮh+$s^~[X}yoݧṵ;k{ Vh. M ؘX^9:j9cho3X?gL?YS5A9cho3X?gL?YS5A9cho3X?gL?YS5A9ch30UTоƿd?uRK>Ҥђ:d,FCSA n ( 5+EOXĄ3ʪ+ \3XȠTO0$n=4\h Hۃ]7W&괽kJ֡3Wѩ#>EYiڍMK`Oj+Y3Yj)g?0<%FOx2jR,!R[M,EvnpZs-7?  Ak@E/851l(gI5s`ytSʓE"30ps@@_%hP@P@P@P@P@P@( ( ( ( ( ( ( .E֬m-+R{[ A+ȅeUIbt2^j 6]O D0K@~Y..2ԥEl|> LeMf ٦@}`'ŚRۧqe5+a|eSp7 dE mkTEҵ VpZ;i'(WQRx 4"Rx4vj;Oq;pǒR$E@Yـ2 MX4+5-6v @ @D'є2}ZV }m 1,,Gc S}f da Kfp=gWgÿ|E\F5+u<v KP ,Nͱ ]NPMky O#(8e8(r# e4|GDзY6KQZIM/qtIY*TI4,+I#GrYݲij3I9&@wYuj\Go}n͘$偊s, HHFqI# 7;UA,O şmBkof2%h8"h$9V- gX&_ԥ!1\=,3+5`V8j?6rѥd[ʑY`Ufw;#w3\L")&D$.}T€>0OQMu#XZn՝5@JZQ̲**stP;˭>)嶺q$3$pzFU]R€>w?%rQ8U;%(2vl$rH&:( ( ( ( ( ( ( ( ( ( (>O˝wZ♗I$$G= Oy [v' ݡZIKyP@P@P@*g36τ6Gcw}ah!L&u9 Lg6cGH~xnb=KYؚ]:YO?dR{M|>eׇ5hf:Nq6bC30N#*da̰+A"2]P@_%hP@P@P@P@P@P@( ( ( ( ( ( (  ( ( (>I:I:VI=ɳ&83M"˝Zt0WW oAڀ>[=.[~.&v󠼷H3O$x\h`A|@P >sƤ?߀?1@Ui]m?gI3D;y\$)oo`(h>M4d;-kmBZ6=8 NHX?O@@A;_fw0:7P}@P@iE6ڄ7a[9n>hǵ}'GHAAphZ(>0IOMzaēP@P@P@P@P@P@P@P@P@P@P@&n '2'4V ( ( (>TkYHGMޣZ*)9a+iTíCqx#y=7ś>R[wH42:e$I@%#ȑ;5ݡ ov9PH Ahz(k ( ( ( ( ( (( ( ( ( ( ( ( FW4;$Co@ Cr+ %4Nx p4P@P@P@l 61ڛ"^BbUGXl° ,@P@P@P]kdo&Hc&:E V<,hDZmm#oL`63\KyeYU|K]27k:&^aZm2&Il2r#"gA3Gom "Ydsc;@T5tg.h Khm.U,,N#xI\毥Ã=ͩ6m <1c1l-K :K[m0 d(Y8tnΪxhx[Tv%#}<.љmټȏ]QUgeDVff fcIbHNr( ф [AYZH6mm!Wk<<R& $1+iLjZ}}gsfչHX|?X]iZ}M դѶxd?yItn08dԕ`XP_ tYO uEs}4и!BĐ '٠Fvd8*€;( ( ( ( ( ( ( ( ( ( (>/>=NdeKn܃k;HNbd)P^ ( ( ( megd'vւkۑmP ˂)Sιit @gFβΏt39 ,2l@_%hP@P@P@P@P@P@( ( ( ( ( ( ( Tg(F|NH~T=(Í 3-m$a>HGPYcU0P@Nu 7O+›+k|~(ŵb;[{HAȊG~_bʮUeeaVSTk_=By.tf=BvjĒL0$[)Gq@0?GOP|83xI#ܱi+ PT&&\Cz'xZݡ o6Pi.JH:1$P%-E4C84i,l=2# 6&c'crkkako'=~xbV϶ :(G?9-]`G0`F!tds,vxsm;S@OWo(?j|;A.#mbcH"NVUV}/U32+udUj( ( ( ( ( ( ( ( ( ( ( E}mۖ16P.٘a2tR+Tlʥ@XV㇚ }|Z(k ( ( ( ( ( (( ( ( ( ( ( ( BY HPUiPP@P@P@P@P@P( ( ( ( ( ( ( uNMZC~,;gG+ȿiۧD8odbFkRH<6m#M]^0mG+(vi/>aoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTN=caoOX?XxS?:VTOIд Icr %&IJie}8 j(k ( ( ( ( ( (( ( ( ( ( ( ( E2\\xw[#yfitۘ3,2qU(B|gBA%TJ? >'Ȕ?|O S('("POQ?5ODУj@!>3G ?B|gBA%TJ? >'Ȕ?|O S('("POQ?5ODУj@!>3G ?B|gBA%TJ? >'Ȕ?|O S( C<:&m4r,v$K9#u(2*%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?|wR~q~ _ſ'٬Z$4 qܦK(nn;]5?|Y>/5_gP_ &^q&$L-]N61K..%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@.%\ߙ?dk'q~dt}}?̟n]ϵK2>w>O7@V*ʺa\9/P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ( (?jڶu<c /(d$Pzֳek{:Rlg@{7k? ƦkcW=A<Jҧ}VH j[[kTh>UP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ( (ڻi>u([~gk6v^+Jy^vZs?gƻ/ Ɔ]CS b#dZ{fujlRlZYZ(4ƪuP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( ( ( ( ( ( ( π]<)d??j+e Y|gk{iky6?k>NBcŢ++R5_:޷?h5 BHo<Є};NtͧѴ+c{ybcآ@XȮn5@h ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (jګG[=c<`g_G5{پky[XXC5 <e]c:ǿW&MBi>Ǻkc+cusbc"@i]\@|@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P( ( ( ( ( ( ( ( ( ( ( ( ( (oګ?g Fmſ<\g_Z$ח\]lub̛,  :ncT>xPDZ+<mw,3ield{%[C[P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P( ( ( ( ( ( ( (?;d XφFs% v p7a[K5x^uoke26)xwMWR^|D_JgU3Y!އk)cZzoqoyoݤZCŵͼ=ż$x&sC4lE,l"0t%H45P@~+𾣪\z4 jɮG4Rլ]Ο ֳ%0ˈ 'ğ(ixZi7NrEPV]jZ[\isx{TҴqb5+Pϧ}@P@P@P@'~7gXZ+~Oډnn%%[ٓyoKv4V[4?eog>wDO⯏>,c[j&+ZD#$ͤZYŚ-tmQVpJ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (?( ( ( ( ( ( ( (,.Rվ 4_x L^#𿂼Gw{gCmYB񕏈-R4ֻPl@c {mt!_Z??KNx[ŎY| eZxItgWG] a,/m?D~7~_k9=BUfBbK<=}ΰ],uIҀ??') wקC|S|}>{}bK?j&(m5I|HD.5{DlNR+@x :O _ {ń m#¾*PJ;kHo'.s8gwbScӿcOgVVWrjwu-x'egi![_kZeIn2QK G?ÏIoí:&|F-҈t{[(9k;̱xqYciQO)%mI|#oL~s:MuMwUGyxN(/1 zLW1K鋨]K}aeN?' c /EOe+o6G+_v~ "k?-6?[ -|d.|lMkչ5?MuIRI(,t7|%[:"~:>&}mEUe建Ӭ/.Vgw0o0o8de!c-C<G#9ko83YxZ񯏿h+:LZ=־ nuo⟇Kpy53iV-Ժ7?r%ܶ~(ˠ:"_<e7ؙna/5ėzn1[XKdUK_,H5<1 iz 3~5dLgg\?F +'w-ޟJxn^53ڛQ% ;$ pZ4*߲ bX$v<*U?B( (j_ڏ_ HO|M:_x5֡w4eU!& 舱[^4%7WV;,˞%%#G&4HG5Imv˛[}vB֨UڅĪ@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( (; *~ݶZ[oe忌48VD[_ EӴ&.YkZ*$u :{2 ;go ]Yώ4?PV O߇Z|4M7~گR j ӣSC'b2:j:Ï71Qվ6A7e]ƾ-c>ܾ)𶛣Ie}-ցxKjbM aCs_|K|SS#PxkY!5O|1qigso6=),<' iy=}$2[ڀx +Nٳs_J/~/>9]hN}'ZdKmlc.qZY@~k4`]~:ox3itxRN4X"GtVj7@ C_اO~(3T|)g4ĺϕZUc$Ԧ[K.ԯ/n={_|75eoG|a1 iw/GpkkV pK6Oo)dU|]WIW~%iV0LcuNloíh#ae,fZ~aY;@M?c_oGO|[ hzZ2u s~ޛ:$7\跚<}K?O0~ / hagWu7Z5w6ڿtq$ >$?mثg~<+xE}WúGXд{Il!{SJK+VIgς> 8mC:~xn˽6w}KޏZe4K-"n@P@,?g/ BL)O%F/$Gq[ЭJZfŐ'|n;hڒlmͭ-[<~P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( ( ( ( ( ( ( (h.۾ImwG *YC.G  PKBCI K6Wśhj( ( (>]j ybO|C=|n7-+KUTUƝny%s_MNJfii__J֟,O/]+¢¶CaN ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( (8_?|F_/5H4[]KVy#T Qy-4e9x/nlog&(x/nlog&(x/nlog&(x/nlog&(x/nlog&(-~|S>+Ҿ'Oτ{+jAmNqleҭ13]ܓmq::rM2?>2Q߇5(Y$Ҵj[;]wĺtkH]鷗6 ~Pk?:w>"dnt?\)ĺ,n;j BbK$J'H>6h_Metu_ xZ|3>(^ANxM޿ֈ _m;ibP @[: 5sxroɤk_ZN뚅6Kk}+β\o+Aګyռ> iZnoi MW [ny5`TҠ,仱,[qm \X-|Ah6) ^ivƃuYokL|D{:Wg-L2r+O"b}goxwſ?jԵ}7cWy6@ ( ( ( ( ( ( ( (1C.EJkhz,lV{)N] |dܬ>0d$𵗀|)wYj: MsSIKl u}OSI[0rE3d![?o|{{OҮ5֯ LM|,XQ5 >arؖ |~~:7~"~ߟ|gQu'eS+Sx;GMV~O+kCλ[\Ju; ;mSVh<Z iQOBK|;ImwnZI=wo&𔛼L6{5 ;gov/|q,x5A猴o?tV[<;RwMKymB YNVUuoFޕ}W4L-cZ:s}_KXyIȠ4}A|^ 7|!v4-Jmc7>u+Qo$N诵-zP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@( ( (Ư/' x8?ixTd >-Y[ (+S_ cƮX+``zX CXbR?Pr%Kh dݲ9P}e_k*]JoQe73EAZ{Ҫq? ]$ 4/<>$&i|x`qn6b(6m (KءTUq?W?H4/(ѿٯ¿dIboY|/cb'xo|WXH'~ xPN>kfWTD ?L~ZgmV+<[|GhxOvsZizt˓Y 4̠ ( ( ( ( ( ( ( ( s0:sV;Q[Ư?4Ito>/}ysXӬt=4 gH8[u//6^kccg=wZuc,5 kA$9"`( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( (lٳIf?>Lׅ&m W|S-Z66(FOKkkyK?ٻ xvC$;=DžtgZHa~A}> <+E[jpAԯ5[y=C_?o(ۛq|}ľ.osPMmB/xῃ|.|]kSo%o{꺉M48/59뿳ᧉ+KOWJĞ+n]ޭoK5oN-R;ӧijob Ě< E/#^&ػG4w (xczvm)u>&uZܽe< ?5~ў9Z1>x'ƿx>Эw |+׈<_ૈ~ i: ޳ɢ줋XtG`&RQfWK/㗍kz|#$hkxn+_"o>hpqm<!Agb6`ђ ( ( ( ( ( ( ( ( :W  /45i5xQNCP@Gaamq}z-3'uŸ'w?Q'xM/Y߃Oa'|׺ltV+/,4+ ";~]YI=xU͇}O៍,>[ u+ .{O]hx^JDU3*PFgS'L>7'q/iLj|kxgᆓ6?U',i4teqiuwueXZe`~~_;(|um_S5k!/|f KxHxOW>,qoXh-{m 'O%°xᅥ_S޵f3\ӵk D$}~O/tYԯt./`5|/>'|ExO?ͯf3iymTM OGk/,^66zSJZ ~/t/x? 3^(Ҭ4+oJ5mJt}[T 2 K8h ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( (m>>*ˬ^e;Tӭ{"N[˪hjl2[iҸ3%-p?N"]S2'Ϗ+X:6/G{o ̚az7zoKS=WTҀ?y ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( (<+ g?/xoqyڍ4kTә|YCY`py[?Əm??l?4{hճh#g?GV@CZ/q?|!ě5$=ΥEGvEHeц I@P@P@P@P@P@P@P@P@yO?#:׌sƣiySgobcu|+#g?GV@wկS]ykkԲ6h'ٴ?Pn@tvݲhJt-tF[Z~P@P@P@P@P@P@P@P@P@P@P@kfZ}jw~j]]LHA$ߏ?oʾ4ա>0K_ȐEsk,)# 6<nxkēN_xSW4ox'Y׼5Zޝ1II"pZ_Y̯m}csWVWQKmsSF@mP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P( ( ( ( ( (  C3ͺ߉-b k  >GA.ԋS$uTE/fҴCUڶfvu)еoqjX,~'sد?7?O>د 0v,_ ?O<}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxP ^ICDlmV=3P&exx9A{Eύ/_ƺG^= H"G;m>Er~'~"_GuMwÚڍ pZ^̯m{csWVWQKmssFWڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/j_ >Կ}(RxPK@ڗ€/(@ <{B OO~)#߃_hA'<-}q}: ( ( ( ( (?( ( ( ( ( ( (~?/ kʸͺ߉-b k  =oR=OMAkڨ_&xn>" _ώ-eڎXO?}kGi>ͥsڶfvu])е)b{P|u͏?<|p;\~sX`~J>q}r>%+5Lvwyа!Φ1g45:?_5(_5(_5(_5(_5(_5(_5(_5(_5(_5(_5(_5(_5(_5(_5(_5(_5(;WICD쬵mVOB;++fGԫ0 ~Zj`/\x0.kx[[?P$BvYmWHm.c$'EM??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??M??_?rcoUsՇ<# v=?ghg u I8n'=ʸ>'L6ac%P@P@P@P@P@( ( ( ( ( ( Wls~=|}o^ 񕯗qwZj=XH|%ڑZd%j9LSkwj~:shz<?C@q- 5,=Xb pw~ .{V)󁜏QJh#~(lz P9}z@P@P@P@P@( ( ( ( ( ( kIے}8={@ɀF~=:b>}O<u[/6þ"6ռ3{Oy>ԏQӤeԭ˛d[\ڀ|?_㷍ٳ! |2q,kI-VOdnZ8'T cϞ:9;l _'׈Br>Ġqz==L}~@2dH~$0S(}G??:Jz~@%k=?E ZOhVZ?+_-Jz~@%k=?E ZOhVZ?+_-Jz~@%k=?E ZOhVZ?+_-Jz~@%k=?E ZOhVZ?+_-Jz~@%k=?EOX5:I3R絻CO Y#)0FTSu_+œjk| 6#\^xVr4Y끱>ǧ%:-T6>oNO_?uwh9AgPgߧ;Ǧ(I<; @P@P@P@P@( ( ( ( ( ( d.}@?S}{0}N{p0:OjoSy9p?~;o.|+pDduxmSڙM{b|2acPQ\[@/2C?R{ǕWK ֡652[ۗY˔:>Ьpn}υn'5mӎO'?˙l^_ ɗEԿ4m1oʧ /rv?O!%tX'?߆&G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_G_ ƫeu^( -.|:AqHmCu |%_1Ec}g#嵳yo|#}q sX|ȳܗ[k)iermxc֛=KG>#[jvinh*ڷ ȯմŭrE:();@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@);@ > gG"}?@ߟ?'./w~5`su3 #=FXr7αpޟ[? ΀^K~P  ( ( ( (?( ( ( ( ( ( ntv}O۾~P7RSԁ919?yd򙯳Ii}]ģ@@M߄>6B¾**yo6LԾv+'}`-Ǘ&R|_C߳ s4GΙi:Qoc&J__b$3@P@P@P@P@P@P@P@P@P@P@x_ukZj~Wa}eqKl$0 91FN6&˛o(k'ĺE_zNn6WϺ)~:hUII!4Z ( ( ( ( ( ( ( ( ( ( (74zL-&vx{>SFm镠pԬm1\F܇2>27)H.P@P@P@P@P@P@P@P@P@P@P@P@Pꗞ_ZZs1@=Cȏ17F2\| g9'NoY ( ( ( ( ( ( ( ( ( ( 7o|_v??p&!tvf<_:P|GOͮiKP{_č> Co.<+}qkj~mKb|,wyrl-->N7<_o<;#O.{隮_Ӭo_ mc}rq_b$ۀ}k-.,Ĭ';W $?*ݙw kzu[:fo%k-ͼUFCe~xÿ9j\dGϦOx'%=I~?_NPŲ^>tv֠ tP@P@P@P@P@( ( ( ( ( ( q~_P ÜoN o!US̸еxѵ?je6]>;qon<6J ⯋5Ngw<6qNX^=Xl.X\52 }@~ P9t2}Zn&,'rMuP@P@P@P@P@P@P@P@P@o_,Znŕj6ZX%QD?+ .3M9-x z@> ( ;G@߉ )6?mb;[+ XUxHAKgc e Bӵ}@P3u ʥdEܬ>8*XR"I@~wzv:oP( ( ( ( (( ( ( ( ( ( qԺ?I~@>Bӵ}@P@P@P@P@P@P@P@P@P@P@P@шm&=|T$˸#`oV[1$IGmj[?4"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P GOZ%"P FOZ%mi4[ۙ.ݸo:T3#b Db Rr~ ( ( ( ( ( ( ( ( ( ( ( ( CR|E-2=Ca@j;f5u P@P@P@P@P@( ( ( ( ( ( qԺ?I~@n3"3xAUU3j*Xd7:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?:|o?7@u4niy?tg_ύOο>7 ?u|LrHnI:*0 Ad ?{#=Ӵ[z.P@P@P@P@P@( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@G_,,AcFa@j;f5u P@P@P@P@P@( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@G_,,AcFa@j;f5u P@P@P@P@P@( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@G_,,AcFa@j;f5u P@P@P@P@P@( ( ( ( ( ( qԺ?I~@-AO~~@5QY݂)ff8 2I=%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@axVOڧtݾ]!>޹pP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@v+Icx}?t䟫uP@|UF4j%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@s%?=qP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@s%?=qP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@s%?=qP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@ #[g-6] [ {Sw2_̓V~֟xF/]4ѭϙxD\T}(_+c-,> BS?<={Aۿ`.uM2MU[|WJu-֔PY~o (X5xLE{nf xҵK{|'UX[WlA,~6_s6O |qsOg_Piɯ ;4WM_G7۬\tpP@|UF4j%};{w$߻մY6yWmu}" ~ͻCO@e@|UF4ja?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i0>a?i09fczVo9@$j/t|Wğ|t{MM'Vow/H'웾}ir'Qni.E? ޭɳʼomx}-Ȳ.>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@U[+inn>U #'0Ow!y A^SXz( ( ( ( ( (љNUe>>|PyM} $61#gk`8BmP@P@֤0C+c>Z.7>0AbHU 0ݙَYf>I>\h ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( )eEhSp~A 8 -.FJ˘pCcl|kP~o|9;)yiq1]DW5y7}o2LZ'PĻC:x;βo`w$߻մW]ŷ `E'WK?ѨX~Pr?ڀ=;Nه@uǨP@P@P@P@P( ( ( ( ( ( qԺ?I~@-AO~~@s%$XEw>ʜ0=P@P@P@P@P@P@xhL;W8C'~P@P@OIt#Cs4;v@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@\_{??n>5e`XHo}w?߽uk+Bb2i|m?oz7Ľ8x_֖m4O7Zi_ZɁԂ]D'j^uM;ZNl`ҭ"K7:Kr/!+o@ ?O*'SBh[.OsJyCž.ˉ|Xρ!Z69b]Mmt[6XfkHQ+ =WK4~1ay!*ʾ6&aP+ހ _3Oo{ c=Tfq㋹|6|<e׍/$8uY5.-~@P@P( ( ( ( ( ( qԺ?I~@-AO~~@b4{$IP^e@8( ( ( ( ( ( ( eرYd`="3@P@x?HtP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@gݿ:(7oN_4'ΏA'~־Ѥu*cJc>MG>@?7#7'cxxO|#mk{s⧋mvzTrws뚝tYh?__?hif? ߉5S?i/Až(O$''޿|hk ֚Gͣ7W|{ۘgWUxz)̚ljCI~v|_=3ٵ̭m1ho68쪕o$@Cz?}OxO x%`|I߈4]kN|EE5iiƧI>qmexxӿ|o??&}kď|x?xqz^wa^0ŏ xzUn-BYjzn}dO'ޕ+Oay>4B4]/8폎Dž_.'F[q.%e.H8F>:?_-R\ 𷃢ֵ Q#Ƈc&~E"+ ;J( (?( ( ( ( ( ( qԺ?I~@-AO~~@g<{1yđK9VܘdɌPYWC0?֟o1@#7M(f{ZɿkO7i&3=?bFo絧P?֟o1@#7M(f{ZɿkO7i&3=?bFo絧P?֟o1@#7M(f{ZɿkO7i&3=?bFo絧PxfR7ȃs3@>\{+hT$h0y%rO-P@x?HtP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@gݿ:(>*`g5xπ/" xg߆D7k1{Kmj11\!]ne?eA~$Ï6gkW .u=./Q}}5l?@t[z.P@VN?k_r? 6_ůګZAO%?T_6]?Q$g/q+=9|L;-x)/?N?.'jϊ_ g|2ռGZME ANZޢbе GzƬ= .>[?o^44;?L|1M;OԖll|SX떖wo,{mo7KB'Irȁ];*( (?( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?Aξ>'wk{tJ.b+V}з7wE JK?,_?8մW "/d@ir2}m@P@P@@a8## jZ( ( ( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@J~#|7ԗ_<%熼Ok;˭}]/YS,SWnwخ}6]QQ#E䬨e%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P_#=@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@uvۊ(>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@Px6|l-G$m#TMֺV>Dh%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@㟍~=e|7͵>YItag˱sZz5ΫrC|$NOρ _B̚O*>!5ռM,{$/7ypǹӴݭf&٨ ( ( ( ( ( ( (8@[{E݃8@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@dyS=#/NzK@G_,,AcFa@j;f5u P@P@P@P@P@( ( ( ( ( ( qԺ?I~@-AO~~@~i|x_6fk:i_metIIacɳqu+gkB内ewp>| WxC6u̾UψGskxVXo*-m16yn= ( ( ( ( ( ( ( (+5Yf?ٖW>uĿ6:i^ґžceۇ<6Ͷ︚B2}u7g[O *&4|KkR ;}Eq[4s]{uP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@|WK?ѨX~Pr?ڀ=;Nه@uǨP@P@P@P@P( ( ( ( ( ( qԺ?I~@-AO~PƟk?2n5ğ:i~žcgۉ[ >ϬjIî,m> aVsx0j%R2[=+KWi' ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (XE_E=T;ki~MhwyIVz͎A ) yAZ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (X)[pV=G_K KSFyަf ɵ%;Gm1&kn.gb ht H3Ȑ?ѕ 1p4P@P@P@P@P@P@PQKqm2{u?:; ( (GVH3e jpgl靘8(:iI];N ($yq`;Y q‹Qq¢(EQp6A#j@VvQ} 1z;˷9l$0'"2vs09vP5P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@G_,,AcFa@j;f5u P@P@P@P@P@( ( ( ( ( ( qԺ?gEzWĿ~?ė-=yg3M:)nH72싹 &;d$1,8ѝUQ'yׇ`}+#oր; ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (X ~SOS񷅵mOV'EtkK;4ZK$4jѰ?O*O'ǁ4c?6ox@aYmJG#oVH`O&Ə7>'/#CY?/ ].ٵˈu_x+HHn,5C)`Q@P@P@ZP%CogfI?|]|Be;66ZOh$uf:h2 <~  /+Q4M/,z^YP[Uݎo:F@`2;GʀyͬF 8!W ( ( ( ( ( (ʬUAfcw.0>Z.vrAbIf#$P@CQKs ;\?\;[pr2TP\Oi!xN +~7!pA^ ({{&Kw1ʰC+X`h8 -C&o}s~ϼյt+q6 w::,- rwrq_c@ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (>*`g5w*}G{Pi0٨?\ ( ( ( ( ( ( (Ɵ/#7h^?Yg@~|!|];KՏo/"H[LmX0ծ<٭^R0 7_*x[~jxúφWŷMKšlymk5 dNp>/߲.eߵ'k?Y^jwZS֗?j0H(1ּM+yO'{N_Kđ[i֚Džu j6^$RwHP{9.8% /yQxuDsKmU|iixwƾ,G&mm˭ER;}:- ?G ~_1gPϪU]ׂ4-|UirwA!FZmR{+H/m@< j%Ok_D_5 W^.EնymZ-_gؿ~x]:w{PN:VvQL|WD|:>#Jĭ[wwyuI|w\].WMFK]"iA||:x!<5~ w`H?q7HzUI,zVVp$W7׷@η_f<3i?P|c?g@P@1U$i"u]& ?Oύa>V i>a>V i>a>V i>a>V i>a>V i>a>V i>a>V i>a>V i>a>V i>a>V i>a>V i>a>V i;{x YƑUM@P@P]E+"+ꮡCph,t}|?7@,?t}|?7@,te#PG/@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@G_,,AcFa@j;f5u P@P@P@P@P@( ( (Ɵ/#7h^._`gd/~Nį4~/x?q}wҼCk^fy# _xo:֕nS?KGun<u?b7W_~|#HNҼ'yBvƣakT͟5wnt]1^ b_ %'O߁<gN'-U^ij*ZrStjft?Ry+ r#P +[ ]ckP?MdO,4~Ϳn?|,Bi iE˶Pa3zgqmm@$[+$5hI׊n=,nWtPj~" S&''摬6h%%;HYPKG@o 74O>YP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@|UF4jCkyO`5/nR:Y``TԼEr]k'TҴy..$.n:+\t?u.5y_f<3i?P|c?g@P@akFs"2r}v+F gP G8hqɟ@%?J?&EӏL?(QN?3(gP G8hqɟ@%?J?&EӏL?(QN?3(gP G8hqɟ@%?J?&EӏL?(QN?3(xa:4tH]eYNU@ ( ļ׬Xƛd#Ea !\c  G8hqɟ@%?J?&EӏLѳ׬F$$Fc *L @P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@G_,,AcFa@j;f5u P@P@P@P@P@( ( ( o xS^Kw>֮mմm;QYR'U%i%`C;o/- Ѕ%?Aÿ4·wBVB_  ^ [C?[; Kh@+!x/ mh_Ht &AKpĺFa .,XnR\Cls䧖U˷-Ã5o+n=Z&GrLdm$: ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( #1Pq~wzv:oP( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@s%??@P@P@P@P@P@P@t>t@P@W &(ye@P@׃6_Gnܦ$\P׎7ڀ*bTf<$rh=r ߕ'lc|;V?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (>%ş?hx_?A?@fr ( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@Qm>i,=7(Hj#'hFIᑆ?Q=dhP@P@P@P@P@P@[D)I' a%,8!cUI&:( (/V-I4dLy*%@*`g5w*}G{Pi0٨?\ ( ( ( ( ( ( ( ( ( ( qԺ?I~@-AO~~@Zh% `1p::p=K Josҿ#\??~-+}5 Josҿ#\??~-+}5 Josҿ#\??~-+}5 Josҿ#\??~-+}5 Josҿ#\??~-+}5 Josҿ#\??~-+}5 -<6b2U2O=Ac@/@P@^{KkˎeW2H@(ai__ZWk?@>ϷFai__^oqgU~b="}` P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@|UF4jG_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j?tx[|5ӿm@. φw-oN<->G_];k ڀ] t[P .j~%xW[;XH 2I5Bm0wb+-wP@|WK?ѨX~Pr?ڀ=;Nه@uǨP@P@P@P@P( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@|iO\x^F6M-w NUW|勾@9*( ( ( ( ( ( ( ( ( ( ( (>U+oAFG;o9% Z4~dvf`O #1Pq~wzv:oP( ( ( ( (( ( ( ( ( ( qԺ?I~@-AO~~@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@|@P@P@P@P@P@P@P@P@P@P@P@P@?fZ= (Xp_PP@|T[,B?lt\ܞ@j;f5u P@P@P@P@P@( ( ( ( ( ( qFCq{1?wgC~@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@|4O +$2( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (~":7:lp]Oa﹂[Ġ4ْH4s;F#,29ʍAVP ( ( ( ( ( (|M'_Ac<`]; MfX-.o;Kgj;`Xf( ( ( ( |1xx4W`x\,J89~ e\nn $.2E Ya7ـ:Z(>&CNJϢzqiOz<5\ct%@=F@?޼c>ؠI{r ( ( ( ( (( ( ( ( ( ( ?o9|q  VL1϶(8~r(ɴlxMխ`?@pP@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@P@Pq௏ -vHeχAmgV֬cc4maͶUwϞ6i|-s,Iմ'?PԮ>K]?Pqs܏]ke[Z~@P@P@P@P@PN#h4,ǡמV;z!Dx 1846 Hӌ0Mo}4n ( ( ( ( (( ( ( ( ( ( ntx7~1yhʷ?nrzz@][8g_\PY >>Vfj: ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ?7^̹-Ռ{#4|,z#l<ʶ|_ֿ71uӟ kN.t;lK{@( ( ( ( (>[񵧙^3igzhjzzq@nj@A޿nO˝ŀ?>c8碀wiNt0qO?΀,@P@P@P@P@( ( ( ( ( ( ao΀2. ׸s9K`{7X >G[mFy3mm->2ƿ獭fobeyv ~)\!um$}7O5+>O.\w#Zn4P@P@P@P@P@>^"8C&2A$@#_Zm ǰmD cG\b:T#?.ÎQҀ6aTP@P@P@P@P( ( ( ( ( ( NW~ ɗ L:w_q@7qdlVs*jq?~6ßaXF*%8 \ ?_D3.n~1ȿ`g4FO>fˎ妟henN? ϶9 ]70^%~?Cp3`8>'`_S¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /^ hoxWCCĿ¿^%5"/)M@/o47Kj?{|+E_SP _ /3|6&?zH-cיPp=0G=h(֬jObo(#Vм48C((V&._#.UWP /N:={Ixm"==O?hNr}ϧJ ( ( ( ( (( ( ( ( ( ( kߧP 2.r?;Pd's(d_C1zuӌ}o{su=z}d?ǷNa9+ݞ};L9ۏ \*/2OޟNp_KCO"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@"_/@'?ďϡkPyr 隤tҸKf8܇[W"~I6,@5 ( ( ( ( (( ( ( ( ( ( B2> 3ϧ>ߖ(OQQ6h32qzǸx_z4q+g\{ $sןmcO (.] IP?LgQv9Ϟv@|:?5@ot:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yt:?Go7@#yyşK ѣDպjZSlwNʻl*^;/\]a~|HGߴ,DDïH4SMg,,u [{;Y{`t#}%@)p1q׻-V}-OU?9#i@?(3R?A d51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~Pd51~@L~P_~x/㟃n%c\:Ju_j6`TIacŶl8ik ˷aBINim' J:BѢ]mk˶[{@ү?F&?OѨɏj?c?F&?OѨɏjoeJӵ? ƃYi4/yƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z>ƾ}}5k@ހc_z񏎟?"11ʞeχCkgWήcwH >Gڍf*[[|K/wgoZ~ וl]¯2t}_Hi>ͦip@}1~u"7@hוl@?c_z>ƾ}}5k@÷ѫ[xsV|QgwouzֻS/&ӯ!O[.,#4Żzoc!Oj |E|FD5A}K_eԺ\][aX|O'~i/A/#P@h7~^2Pmi+"# Ȏ3p~/?@x>u A.4_ Lkڦk4?(KLa>Z*`PRg-_|qоxG~+x>:|kw4W-/ _WևM[Iյ_Rh ։Ago~6_i>x_ڔڧ m_<=;k2hWgtqN=R?>~ ul/ |wj9Ŀ Ҟk+ VX5_]٢xW:>Ϩv򙬬l5p'ſ^%5wہ|Umx 7'ѤӵkxYx6FZ7hZuài2bmckG/uM÷>Ҽg;M.Amo+Z@nFj/MCOaB|]\g[.|[|%u߈3|-_x>"QӵsG4i Yt[Z~3hVoէVXk>Tm`^VG̷݃řX |? x߀ik F%X;OC8i& (yBہ$Ҹg`Ҁ ( ( ( ( (( ( ( ( ( ( ( (0 ?*6?>b~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@hؾb~l_Oր }?Z6/@)?hO/Ɩ{$O6þ"3x_X1Re}|$z+R_"`-Ÿ~>ͥڎsZiӥփtK[@ԭ}?Z6/@hؾ~n7O9u׈|Ykz ?[hx3⦓g8q}SwsCU:uZtX_'RSO?7ĝgz5~m;M{Wv׷~'گu?[_k0ӔCX(O FI^oM>,M0~ir61o![;6TsEKyJ}o]0|!Aɍ/sp%Qܰ#,lB-ᴶ 6tb8XjĶq lK-,AB[¸F` rO (dqvFiډeU2kk{2M[F@&?d9|zM??h> _xY|9cuZl]5hCwz[o$)n.m?x #Ia'Y")AؚdK (OO߷nm_<#>I?V^NAcXGCj SpZŭ1@"sao(?un>|=2c]fu!3'ovWt~߱d’-{2{o xޣ LI]7IԼ;qgw}?6_|'C×~#S_?ƚOI~ͭZH~`!HG _\;;F״>35K]O?^o>ڦM'Kn l4oinfԞty_|=_?Oe7W5z_߄KYV61Ox*( -Rᮖ D, ;2>~ߵoƏ~-|KWźN~"hMq JK$[<%KH |oxѼ_?j+zZZhQ0Xܼ 7gx$柶k8~)?<{'?|E׭ |QˤiZv~,4ѩi66ziҭ.{urj?+῀BC|7T N}+5Mkkem50vIp<6FV$ ( ( ( (?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ?_| B^5"y^qcdZ+|oPӤuG>u8x[.ZkԒX̟fGTk-4[bk{m. ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (~; ɾF{K~JmMCKmP֚n:[6R$H~2hw4@u*bW1@;scGʀv\Ə*?۟s?;T÷?>4w|n|hPW1@;scGʀ?N( ( ( ( ( (?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (icingadb-web-1.4.0/doc/res/continue-with-preview.png000066400000000000000000002245031516124654200223630ustar00rootroot00000000000000PNG  IHDRMc:CiCCPICC profile(}=HPO[T ␡:Yq*BZu0y4iHR\ׂ?Ug]\AIEJ/)}TkP5H'B6*_Bj!(๾]gys*y>xEA #ymeTHbK!@F eT`!NF4'?jB!B&u’ZVkuB!B;=45ٶbhjORr*!B!ݮ=$B!wzhj-y޹CA;B!6mqRqM]ŋ,]b <}7"33Yfɮp2d{FaZ)((`׮]I !Z5`***(**P(ӧOڙXJv7F0TF4˹|lܛf^5zURg5ߝ)#hqa:s %4 AdkDqt֮lܸ={_\6mĬYP*o_>}ӧW7#Vٺu+ϟߟҽ{w֮]+IѢX>v;7nرc :Cb9s-g9uy8>C}2k^#y\tr__*Epظ aԩիy0L:>}r!ػw/ѸɊ+2e }!--1xݛٳgSQQ[oE~غu+eΝTVVvZh gȐ!\.j5\.<<||||8p&!U #F **ꊓ1cƌGZ=0~lo濜I!yҴ\Map<wRhғϫZQHHϴfOzƲĐORNj26iA. i^VYKyj0ۛ>AQDaer LB /.&@ƽ)w,˞nw|McgϞEХKؿ?EEEj^^^qF pQZ8rڵkٶm={rQUUի`D\\'OԂQ*-0]6ǎ#.. PWWǦMHKKX>B\>UPpYKV+|[x05^eɷ2b^ꌄP{8eY-Ǻ !ޯ_~xh0sZxc/ LT摶b'|A(Syo'k;wc4Jv΀ۼ{El_$&!̊j錉?m9EQ-ͣx痦:IV3}t.]ʚ5kPTxzzNcʔ),^/BAjjkP0p@-Zc=ve*L2>NPPǏt0w\N'=TUUkhfĉ;8ގtY^^<|\xv͛oWB'Nпjjja!11 .P^^~f2, FxF0T* SXd.sֺQG(g&f1d%KxgӅ0!5U-x4/Le_cops',% ] ܟypցxi:8ؒby:&-ˀ(~o9P ]}[NT>?BEQEw0ƆSZs.QkC7\UCR槶,Xб*^^^TTTRTDEEѧO<==Z㩸zs^^x{Ƨ>'zI !v Bv|{I~P(QTTD@@.ϟ端 !!.\=<<:lpPTTD?TUUP(K,]b`j_O=I,ݽ [}]4x;ö^Y*C47%yl^-ਣ=!Òy( 7Q!L}!|=Ȏ;0]0`52P ~s?1b `%>rWXVoZ2~$fH:E2kBx[-a|n>DOMŠ1h2lx+;s0jsSgYF 78Kp {bxbH[-'o'l,a#d/NTF^~F%dJƾUvq[)}Ns/DJ%v>:n 0a#FJ?ߞi8i#B̷8dIϟ'77ӻUUUv+[|-__oooƏBwyj׵O?ɓ'ebbb l=ʱc/N'_fq3f̘ۺ?~?͆ ((g}GL&<==(ܹsX,jL>ezL&M$AS\ATvZ ѷ3t8C{m{ vjhy1PY E7&;SA˯i!Y0b&[4{r:,a0r&??Cm!+2kZbqjn $d,!x1?J+/<9MLŃ,(*7bRbe_/7vQ|MHv"q0gTUVp*QHxN ;NhwqUh"r+|eښ7BQ]wp#wkli|sbr)6eQ:s'c8fq9t:y Xt0Y}<=fiG7I M*=dm1j}`Iz>=m&.G,_JT;/ӆeJ8x,&446񄑧ÀqfR&ݖvp$*ydRZ[pR.7|Tnc яtP`UʹQhGH_ő-W}3 ;vwCϑ=3p^Xe.YvB:ޖ:)gSւN~f;i<z56{0#'6AcF2X 6#ϢEk݉L,;NՙoYDg,(&)DU䀽ɡײFOWvhGn.'rB"wk{Y.i(5)~,8p uj$""BCPTšѴ| fv\t:9p4$^p_vۊ ti߷hgӉl 7n3i$bժU|7mnٵk}*HT*~_n7tȲs=rq].޽RIPP=Xc`5={VKhh('NbOؼy3?i%ȸ\.2339s 555ƪ̛MbBHݐsDtAcǩs3[OWl<oaBۓ̙瓶"Ma*ظÍϥc>$5@;hcY#vP=%œ6-=֏ X1cD_%P䠇fK7 qfޚ϶%1?&|;e8^>V=E`r v h|b{5{Ot@}h:[{$t`uL ڞ1 ˣ'ꁑ U&*,j-v:[ʹFi{}€#lĄ۸YDurAQwL#ʫ krJ~Y,mo)r*Ybf_  &PAv[BSY>HiZ X vˮeU^ԾQ3IvJl*InbYDM"ɍ,mʾ&Bk.SdUdƔ{ L= uJx?sSb),Ĕ, ZnjbaL-/wT4 Z0 Yc!Xr>XCv,P^ʋE]5jh6-?Q8%me.;zp>Hm٩gQ/ddJV0 MŹsÈj`С7zŃ>K())!66k.^ϥK:4%$$FTr1L&j6Wj5jUnӭ[7ٳgNqٮX,´U]]v{YBɓ۶m鸹a01cŤQWWGtt4YYY|xxx"zjeСtڕ ՚w IDATϪU(**/~  ?(**",,L6#׶P(ѵk)|[{&rɂ{urrPnچQ 1~}eⴣ120!B.k/,#,ƈU z{>]0vjzG.SAǻ&% &Ž3r2!G(Y_NdV|؜,̔ܿwC[Mh?̇A= \n7O{- H8эbL=9?幭2m !X"t)4WH J{!1/t>GdHb"AM x$u8t__ߥ(ؕI$׶ԕQY]WWnF?(mj{ep=F2~+J5fqԖp0'~nÒ}Eӫ 3"ؿυؒl2V,'ft"**.Vq4Ҕ@-l`)BPjGS_W㧳`ḨhQ;ଢªǯK4||׭D^uTfV `CRDrY _Ϫ7S待WCclA1%?IbYU`T.FB}[+!txX zz):GQ$4$ FлwhZԠRZ*\.)++f~|#%^v fl`?Xu}W-WBJ[VَЬgor Qo^ko)fpȵ~HhI* ooo4 nnnEٳ,]BfCTzVÉfi+LVVN{w;FϫˋAlٲiӦ5vkzD.]0mMj0?Xtѣ]6p@ρػw/'$$__ gDD[no߾]w42JkFHHe˖_裏CQQ.]W^شEa|֏URRΝ;9s&^'4hǏKG;hmϱ>#00vԻ멺Tծ֦KUQp<{A֞ 4UP܆ =gC-l^ZЕdOQDhɅ_1#trd BmyJ*LӰ|9{Є&]0Fs݂uhϡk]K8z2 yt6 ?ߝCʾڞ-k˗Iz҉DH !~&pFbXePR @rB_TSXX 4II)x1#7=6HH"֑tjq)P՞ixF160/!W "%\d ̠kX &tɋn#v1 or==[>Ip7-Gn2VhgD $ ?B*m,)݀\sLTb>Obnշϱ{K6uZ5cFO(mYjO-Ժ@HF =Bjl=cI|F2:V>i-~,Zi=$_,c/׵+44Vc}d|ĎfP)ٽrʝH2ʖFXHFa#ڔQTNSھ , 7wpċx-kt|0/ņm ;-RrRCHu Ο?ONNNݺ.w{BSG״{G Z{ryJJJp0bĈ6OŋlڴS6&֭l6cX}FiEÃvjE1` AHNNn$ TR1p@֯_ϣ>ٵkNÇv{aǎ׏Zt:TTT`2144 jkkqss_7Dn:l6$$$0`?N.]8<ϟ]􂽂),i-8Syn] YF63SɧFaZ:cLٻ<$qy9dc3n%o}+)G&͊'xRXʍ$f4@A-B0e24.Ē8|.aڎ{Iq6'#?4g͢H~8 oTFOO{ykn QM>ZLrNBTEٯMSjkx5jG1apl^[7_`q-!ޙIn,/<9 Y&3c7֡o ^Q 4azkT@cRHm~1ٍ` v b;Ӧ%)-WrFs֕0i>7i}=4}Q|HuuY &e*)1WȇJ7Ō^HjD۫θtVGeRqJ?G'6 >-/w䆥6~zJ}آtV,CM*Z:9R]>y7}TCG2`:K>؎k(>CJ3up C}^Ԁp<n.kOqԩVHII pJJJnB>׼imiȑ~Qbt6V=߿M \ee%<裬YzN׮cӯh0a]ve͚50nܸ[:,rKWVVy ڲe g?os`0j*Z-}a{wEo /@HH.իWS]]3ݻw'%%Zʕ+9<!C0thG3:s<JC|6glj8]NjI$39OVP CVaWEFu@ŏB-׳ڛc"X;W409$y!Z ZSP(FP?4EM 7GCTu55`qS\.VqQ*d0n;N^V4l&!Z42P);iijBIYY[n%66GϻVKJjٳ777j]vQWWw^V鈮Bouj,X?2k{[*m|SI_eL?}4 !qiix( )u8|w_t[׵o>0a„XC;Mm~#n]Eե*TBv3$]$4u&ĝ ::?ORXbvl.Trɂ]O.ݸ]%O! JᐂE:kv!Ŀ0BadH6ֈmr:?F=*u`5B! ///t"-"LRѥKPk[u Nz6CF ZBф'r# ! =79z4 !B!5HhB!B MB!B!I!B!:\t:pGBq * F/JB!nNtH`Bp8PZZJmmBqtjKb鴰T[[)--jʖädɒtŝMİa-pPUUE׮]ecA)SRU``a#B١ҥK 98qjj5s=k>gϞe֭X,BXV;ٳg3f̿ehܥ 5=%/w/.UQRY;9̠g"& MNVjmۆl@PPWWӧ9{,6ϫP?AFkM8N}TSt]ܽ /O}OBqg־N'~m/f;v={`0iӼJKKe uuԺK[ LI'2;av=oq+K_eH-cwҵ>3yU޳n|l(ưth=O*8J hCHHN%eX G!g"d X Yb*&afA>?y6V@*#n۹V@;)7? N=ᣓI[9b?L۹٩ontEΞ=ÇG?HjZp8L&;F\\\Y]]rpssc5r//1GϏD/_.Ǵg_1 srӖQ`SquP0z7cݗP;HQ] X`:5FF=: [(ؔFiP;%11Ws f{HNhl^ɩZ-&28tVVoDž‘-^V^I$EYgs -0ޟ¨r}6s>LZs`Boj߱cڝK]4fc/bɚݜKhi6{j1߆Rl6r l LMՋ"***p\(Nn={K.ԩS)((ܹsםBr]xVV-)wΝ;XS}e-W:~8C{m_Z Ms?/Y8kcBpz~"HyR"-64F]dK~ON'޴;F`-dBFbM˘B,Na]ԍx%d@oq|"'ZI{eV'0ﳉ=Oآ׉9eQTF@L 3_L"a˾^oL۰|먵7VNvCFPr߰%Ǝ6|,)(OMsTDJagmZ d<\HJizꄀ~p 2Leߕ9!dscӬl~mi)a}/z_ܞ9j;Ou7/'{q:$2RO8`Wc=M*;ټ''_! O=Od4, Bڽ/N``#Ǧ1K}hn 3H Wc/N=Q %RYd\"uz,#-}S=BIz`ޝG2ӆLɱmGÞBFel/g,ɪf(#/Hd OaNfZNnd7vW+ndաU۱i!qB,lZGnӂG$lNXȪMdϦ\j;Ndd5iF:Ss*g5'xQsNpg~3ണȤ-/|-G< 3,9(Aێ58,w)J'(BjlycKNmXHv,-gѣ7c0瑦-pjR|jv^~@0&MRc|O34]ŭT*j5*q>uuu,]ѣGR۷NLL ^^^=z@֬Y@||<_T gǍG=P*ddd`2{oj\.RRRڵkk֭[f8NN'ׯfs?sjkkNlfÆ Wŋ9w$77n>v;>:y:D`` 5557˗/pX~7#00 &\XeeecWrr2yyy'}7O? &jQdggs1Hnt|\1m///Q*h4lBQQQh4ޒ}t2qD|||jٳ;w^*<==p*U} V{պjղedd\1͑#GҧOf3J2UwQYYWӧ8OJJ }:rp\|XV{9)))VwUСC:tB1c߿ՁRKŅZjsd^ٛ_ -)*!K#ݺtLVnbTb QLl^i*i+2bB~;'e'189 NNFNgd U,Xaoc̼L !/ 9LԚ/ @WH~t?v897dxcXV0 mO2g~ߞ_|Xm`ޱ͸2VFN1N1bg㢪d0   LT[q%Rz}emvuݼZjiQ&bbbJ2*?3 10b:ct8ss>9y?ڶ=ZX`2JW/́GȐ@g@CYKmY^ IDATDRD&5C‍QsɒmdRAC[Xb!iͳ/&ʑ ,T֓ oy:~N>w=5K9lyJ/{``P`5k8P\2jkO[:BŎ?~euo FFХ}|mX,A~C :#4r=z. 8waF[(l2bpOوYf90F6|Jt9$NFϹWWQNgh? #ڴ2X\@ &ji" 'd|hh6%M8 j YA)9)n 6B$şkHhz=3ܜ1Y` WGi2CQ`55X Qf$!TrVǡD)NXCzX_e(Yl>n C7R,ſ.ODh< ԃ]mmC+e <3 =@>00l}^SSbĈ%8z(>(w}7III((66@6okFBB8N"""زe D!JHH>5f4 INNFPƖ-[زe I&M8z(~;{, .dϞ=l޼>9s\qT988;voAxx#cڴi]۱cL4 ȑ#)))!77پ};6!zhhh`Æ ^_V0'<)::)}tt4UUUr^uX`n-[~~\Vq0 lݺ!Z-*W_} n^{5̙3W[XLNNfӦM{\믿͛q8L4i{-^m۶aZ;yx9|0st2rH/ ax `dzL8Lg'{Wyb*{:_. *kKUa:a\1yeW„iz~z'`6P|&'3-ZOb\:UG|B,t`mFb'~z)鞹C*5ꑱGB.9cb6T`(?AU捵RQnZƢ;SAʉ~Ʋ~χPk0`i"I[?% SIm?ZUع!و%n}m&l^jQF=D>&coth8 QP8]CbǾzFt4ݗNȉOt{77 eNZ̠=wLEO&m@<ʎĶ|*,塹C_>ܝ6vU>̐ly[kݪJ z LR\׳E No8VR ^͛urIdQZԐ&ef~ I:KaI8(ۙG<+B*8w|6nefpL7|w.N64ACUOJb8,fiQ[0ʳ21i4c2a#7lfbr p(I?hќ=N Mf4: @#.hv L&_b-eR =MAAAh4ZZZ())ar8s @LLu?XƎ˜9sشi:(~FtR__OwwF#!HΟ?͈#=uÁw_ŋ/ O"?{_jkkhD^줤oO?*==˅F{ZbV+ Ao8a7 wމ]]]l64 :t;1cxG}||T]]ʮbٲeT****8uQQQdJ s=zzPSSDhmm@]vr|кE pK]b5 RyrR)܌dffzͽeۿӉn}%p]wkɓ8|0*_wxCTi+6Y 4IISy%%%[nǏ_5+Ÿq9s& ?W*{sn{/aaa_Vɓ'o޼_K$+"m >>>^ӗK/ &2;' {3oP0)}w_50F/e Gţp MQPWvؽ0v nHH~T&d!P} r xi'b {VU_ѱFQE?@uh* )D;~ˈɕ@xOl4%"?IC'-yL[ FW=RƯ^'iםEHQI\J$ +`A~@3#8{ (nQ/}R@=C^"BP&O$=RI/A^)TVAԎ|rk` v[$Y4':]yiE'wJV1?"QWB)YIcIW)al' ȫKbytVbJӢX(eڬKF(ސK۱\z[ {= 1a44AJ,5F!4chu7@45rfh@],#3gǏ֛NVͩS0 vO-´ix7HRf1{l|x{2{l:;;H$d2.^ S]]=ը_יhn?ڊx.\@@@>Hq:9e&L ?1hUהFp).\ڵk_=[ nw.c[[w&33{Զy#&&xZ'6<}Tŝs'/ f489 Vax1XjF}Lˋsn(E3|QMKl;VCp$ܲ IVnk8Gl G@M/2H[SeeF+PX#K@;#`Ԇ䕿eEF= =Sy 9IntO298,&\Johg{4e%ܸSŬf ::#O 넸;0!|#bΰ/ggM^^6 =كdדqxPNg ~KWy,:j-otR*ߋJ o$ ( URB>&-7GM1>۳I/07*T#4I=8_#<~r_v`1].r(hUUGryUqA~'&&Μ9þ}%99m۶yя~DHHנfw}w3jL&N[oEpppㅅ1oޔrx|\.wyw1ׂL&?8=ܰ"QQQ޷+zg͚EJJʗ"yo|ʻq2{#:y^ W}åTQSCr3;Ұψ}°,V-ɾRy,L#G2K)8`@3ySKYun#/ޯFDۊZ Z{%/2loCg&tD;z^N<^L4I&5o J(˽uM;v,ƍo:_h &O O>z:IIId21q/k}ʓpomdܠkM=hWݷ̟MDه, 2d禓"JAװul+1`&Y{4iDDu58. xr&hoJ&Ʊ{EbHLLN.,,LA }DoBM*s뭷sC$4ZrSVwV`z=VNY9“fsuVP!qj民'w辎"uRߦB{0Ѣp\W8}4OoF=E.1f֬D+7Z_9*wl:Sk@嫢:Z;Z9R}SV^[=7U4 pS_zTUUaX|X b雇JbmZratYP*`D_qd2/zo)X #55f2p1jLL!DDDDDn75墾)uv<$>}WJ]BL"+oS"DBXXvQ<\`j !,З2;#R)r`&rAH$Ŗ.'OOTBҗȠb*G* ᇿ\@@}ݸnlN2)EDDDDD$"""B*!*(`%u&+-\hpB _U FH?R>>HQ@ tDəFRƄ0^(DDDDIH$>AV5SjbBUc;uJ!*4H%;-.ZJLlQ4|Sr)CѪHb'-ԶZ1;3wxԩZ3r)jƆ Vy9T arii⨡ NVkv' ؆"""""h '$R?9J ӅNE ;hI ml_.%d#VF#qP:nd Xm\3R勏'"""""1Lb+|Ƈ(f BKV;UMںں9q/f* RVB& ^nhf>6pRbB#wDDDDDD4Z"""t€ё..7&kUXlkjFK{ (2F*L|O.|otvS~j0Yq}>GF52FDDDDDsIDDD䛎[P?*NNƋT5QYv&kWL|}1yoZ8rvk>n.BEwJ@rAJgԴP/vr&s*^qsM &u;]ߌzjneA$\̌)"""""h\A& |`jh97b׬..d1L5pT*ڻ11uxyVNYI"PDDDDDD$"""eL|Tns-8`OV=[k4]]dm#}UҪ p{Q)T/"""""&)/'oaԴZ_e妣ˁkTMJ6 * В9.MG66mmp:=i[Olwo@Kٽ}7ZM7JtKXqW"u\9Sggv^~%ʜ}-u9|?RYͿƍLtj@g g[X.|'(D{FOw RӁ |F*.BGڒUd"wVa[~[l}jEOT1aҭXXŬoP* xYw v`<]GqHvP'hN)w`pKIܜ%,/Q4 S@ɤr`ciҿ3E!`]+q<VžC0GSj Ci}¾3){rk')q4#T'd%9%&kQ2rܤD [ʼHN /J$tv;vrI^[@?M_ඪ飦_C_a#a ? ed=Y  ~Y_ˇLo5S [rȄfihlX{dfX\bGld ?[x$kߒtr^/\,L{ܸ0iMР7*yOASޑFrLSI$_jSl fQTB@fTJ'v?4d:hh2!NSPFbjϱh#LsP0*~4?,uJѱőųτ?2/MPKV1KRFb.9d1>axS:GOX'&zltsz]󵜒C MxysH 􈝒!K[ê8GM1&'X{׳hf .iMddJ ˉTE +dǙ4A卣Cr9qrey)#b ɹ? T&d>ycmZFi" E7 M> `(\&d9I{;P6O]Dht(y&A o_Ĭhǡ0v$d<\PU 7ՎX~{ {NZ*׀C$%$yDYh#o)z63/[@9P9Yd)1}^M 8@ȂEp8p A (W bt:Ye7f,T,l]E3v`[#ADDDDK (A*W&%TOh-uܟ63+9ZFz.4[8txgx*9F{n יNW&%%6ȠbCe΄H$EWI PȐJ<[KW2n$<0A][ݐPW M!g KyX*#?w/ 2VԠ{W7L8= 3 (jkI'H奝riDjA^Gx0ďڳ N4PA9h8[EG&vϼIoM剧 ]ѧ~4{ˇR&3fR507;m{($UZ]X -q Ⱦ?ۮ7$o0,j.Y|SPB롭l,0a3K%uPtY_dW1}!LGB@3ͳ>izBgTX *CUE#IzEX#!@v 1tV`(o*rY!|Rn L- Kѝ)f~Ʋ Xc=> ]$M aN|'+դ6f[EI@h8~(Րv_:;, RF,:B=wL%xB)Pa~)&sCF!l{ܶnIdb $W-}df{s"ݍ%\g WR5ˁI+1ĶTVpGԕϽ0@{[ _e,Y+!0Rդ,$8>/tkeL*8wٸǁrt*w0ho2~iM.iنFs^zR="bciQ1\`T`plC^"jL$zd#||NiR*ij&Վ$zAT!d;`m`>iA=p?W!XNjP! rBQeX y ^N!x!EV'9~+TNOhԦ1mt&5:# ac`ˮb:CzAELn+mE/}5+$>=4)܀BpXP2gOQ^&Odfq¥0HRN[!*h%xݗ<'cșpfIх6s`#1^kY4Pđw54XJ XР0 Axb*$!͎9d]I%yL̰0%(kQh{j3RyA(8Wc$h΁6M-zHKDcȪ%A}hd &urұQKI]-(J@K E LSB[%*NBV`j6AZMXFH8!U(l՘4zWgk&/x#rJt*" \C4?D%ˁ!P7)I$|ʚ&i n-+ASGT PJDEuDoo'"""'O@nOi,11LJtHnPrJ/J^/:͞5mA>ӍGe5TȥR&s먑F*F<~]Uی:"#A05rx) R Gţf; &(i{ wƮV`,dwQ+(*OУ;EF 1K9qI$QȱE&5#YB jፀ_i~ПiH{!gK)klE(:XCÉB줭]KC^VPNPڪ *!;l 8yIRDMO!vc[j6?/0U$qot#p%sg!$of'y[K'#MټX@r_Y%uq:|7PBv$^2F(ޚKy`V1佞Kb `*E^^eM擗Bqy" !@t*R'˚pD]'M4Pnl/ű>hn 䳑'sC +/!5?a3pd+ wC)( h DzeH9>2JͺCv Ȗ !!n9 俼䟲"uOxlKO}c/m D$ߥ@߲"o#ߊU r~i;@{9w<ݍy6Zf=8 _P"9K $0kM@C 7G1+;Y}#O ?pGqw,g`7M[iBOz ^Q=K/x o{Fap("$qd=)9{gS/')p wEAKEBB:goGPV$cN oDF$ PLE;(:.u'|T%*7_J2ӣKɌ-<}pǝ G#8vR(B""""'Tn7NkAYmEH/# % LJag{lMgU6PQgSFk=.~Hz.[k"Q~rdB=hWݷ̟AQOSŪ%<לVj>/9zmp90<: ׋m#"Qޓ/++:ΞWH`Gˉ'(=ub7|\ZPխV[,mhC.xBFR UWU) +w݆܁j|s;Fs62)1Z &e`+z+0:4kKrJ92*WL)+ob4=$T>n4'>7RLCA^۷tv(DDDi2L466b61rv;jZMHH5>| sܘ]|QgB.]4hv#$R r R}oz8]n.v1wtapau#J )&: |җ4tdSWE{W;umuvr+$"""""nsoh:qmmm( 4 jFdl6c2DV3~xEDDtpvf؆kN;mvNvYCy-Wl)dRBF$1:V\@"/+z9h8HUkumuX,(}DF0zhf/$Q4}DSWW#""ī;~8?@.DDDw.RӅO栩nNlN6I%|hU~b/rArҍM""""""7Z4}s(..&%%e?'NDqAfΜ9lvٴi},[bٰ9{wѣG_W>,.__IHnرcpxRE5~mjZyGd<3h4O~;h(Ǐ_򗄄xu:$&&I[Ogg'+VP_K]pnԁ*k,͝;sPSS'|ˆ#EӍĉDDD| 8uƍhiiaΝ8NX,RQQAJJ <`ͥ'x[nCý˖-[&""w|3UUU7/&;;?Ob ^~e9B@@˗/<3y']2~V+;wʹiӘ8q"Akk+񤥥kWJL0={3 (**b̘1rN'?8?>g?c8q5kINNfŊg}8⣏>B" HXp!Z,Yp]w!xؘ8q"8p[缼{eo'88ŋSٻ{Yaa "7b$n1jI\IjnۤMs~Iuo߫v5YL*jXpMDMPa``0ECb>k^p<=<O111\xlم |L&`ݺu:u |OWE^Ha1LƎ9HAD4(hh4c4fxp.\)**Q}7t穬$00p#HMMӄIRR$$$VINN;oSSSCEEEEE!H$DEE&on^π5XttQT,[2ñc8FTVVӧOsر=~8,̙˗>99DB^^/*--ĉWĺuزeː̙ |EEE(Ldd '<S9k !2M ˜=_0tKGFF3&TAAAZ$hh4h4JJJzΜ9V%AAAAӗ*==fommmF^AAn OE;Ec)6W+<~eEt}], \.͛G||zs^/&M";;'Nrɡsα|1 ^/KFF5g#4LݻTL&cɒ%R^^NQQrV… dXV>fcժU7,+SRRT*=#lL&Gt_|N';w{E.鱫;wvq:}vm7tI:AuJ Jx/ LJ#;!uM05O/OFc嵼f&Dft [e /m"sV(C,a Pg bpt 񩼘[/Ru/*e}j?8jb_)~C0V6PΫjh%6#[ r7uVPnYf͔9VPT$sfT32&Cj%&{0f|)뽍WǢ(:c|Uɿ. n„ X,F#&*J%N7qDRRMO0}tON~~>,?N@@˗/ns <Rŋ˥K8r>&{1n7_7np}Yv֮]f˖-](,,$''ӧ9y$999?~+WΝ;#883gMMMub28po-ZFCyy9NBTw$677x;ng<̘1$vލFTret:'NPZ[[Y>ζmۈ(.v;yyyx<wqnOPPo68ַB ??V<̘1rΜ9CXXFL222nd2ٰÇdggGKK uH}}=F*HWW&M"--|HMMeʔ)lo| !((͛7smp8GT;,[`OYpٳ={̽KDDİkoo>`0App0]]t'O"˱lL< &PPP@MM |ZӧIJJ";;& oXS7t _Cv.Q|<a{~hvكX~QGQԉ)8zjZ*r]ⵢInE]lK41g2W Cq ouL"{.RaekB.p@(B iUwitO8uLVꂵˆti8Xf).mڰ1Hrv_UeՊId]Ul mW_6\LWHa޽T*T*qxx?w}anݻwt:d,T*eܹd2{m {iii7 n ɓ9z( ax<l6{A*ݍne2vCVt"H| BnnP(P*d2BBBH$11q{f&|rݞ+V`` 6 KHHRD{`D\.7RWcER!JG*xd2c_{6hx,6ME܋|%W q֨PZeM@Ĝ&x01DB.+mbvXzDՑṖ].f_ψNY,E'y%t~d8.=쳆4%zU!+m>ht:㏙?>RE {JQQQ>|ۍd21g, &L`̙|GC:2j5._RQQAll,jLV @mm-:`N'n`Gf4:;;5k>}b fɒ%с?&gxz8qSh|eG8p8H$`׉ $''˅fϏ֫g֬YnV+jt:AAAXV<nFPP \vmXTJ{{T*%((˄ 7"<<#GLgg'L22~x5Pj^D ĄB%AOJTOx==%֠BgI|yYvwElo`:P_>m NuP`쁂pHѤL/JkB{{ Ntӭl[r SdWo2irj΅eֳ*!q<F17 xןEbPT8RxP'+Ps_m_ +9U+&5+ir\ٳ6^/SN%##ӧ*hI&1m4?f}v,X@LL̘lˡCx"^ɓ'2Sn 6pw ㏑J( _Ǐ͟?v XON\\ܰ娮ݏr%""r>ST*JEAA/_ >>Yfaٵk^ANNCfP+..;GRYYVy捸C={n\.oĉV˦MtvYb JJJbڴi;w˗/3o`4q,t,g!P2)4~vj# D q[wdw0,zQ="hA+3l7zPi ﷲ/E,W0 je {qz LNJ9ʨ+<ƶy~MF޶rX48?&_dv`g'Wgoiujp_H$&/sD>4V&' #=}kjQ͚JcEn;nϖc-T9GH;thc}mb6L&׎v2se5{; 579m "K,u1p{p.9L}s_7)<bId C~׽ᖎꡨ.(8Fm!qar;hʰ(_a8mᥭ tQWgjaY])ϕ?Kn5NM 7Ӈn>e"OuTU%:Atcg9uxeIj4j4kRl.Lɀ9xonTEUy9uyey/UV"+XV啱?g3Fmb#18.U֡bL2~Rjz ryzu߻VtӲy,E)xgI"K0;o/~D%Bq3xz/h=WQNc\Ko)sVѮnlэ +Xk>7Nfa3鋣D^M ?<[3xzVVǛ,]Cty !鼒A-:v Zi:ifhȆC-X$Rܻ22l<ۍХA_v}T 3X3*&1 j eE\nϖ- b0Q6y',BiB%/ 7 0 bzL+3&u8Ʀa[14Y4 !v*[;1jx.ԡsv"yR$\6SjC=13T&ֿJDk V,M%&U &u4f'}ep0~[S- `Ѽ {];Y2G g٬H@YM_fSʶ(qBidvoo9G(=4x2Efl Lg3veW_J< -R^(SKcc@ O/ҏQƮ¼lԟedo!]s;#fDdDOHe%#C~uđHy`NvsY+ *]1I,Oh@?}.>WTXΗ^OŌͬz D'&<_*wyeJKj t[ }$VH;ٱ= d8ɩah. |.!C!R6;tYns2Yf١U!l19&ɓb<;bsfV7(HI, w޹ɶD?i;Qx?0Ց#*3Έqh w IDATo.SP&eŃd)ԝk X88 mTQԡp:d^[o@i/Ɋ7ǯ-_RcQ"hAIUbÎ"G쇺ֆubdhw1(71a}AXJ](8AbQ%E ҥ!b/bI%ů/KRc؉zzjf:E~F*-Nl.'AAG pjQvFdVu~d;r4ڠ@OyBA7URNϗ6闓{OO& z7~L&EPgFNiԅ:&dE(VL"k7g]p`ɎpfxEޏٳ`-ɽ532te+EflAA]JCI`ߧ׎MjClT59MvX)u4^ ɕ}~IMMoFTXv?K+sxpԞV6VN2URTJe!dŏ&^zN9% Aq0 MT֛i ԲBM_EۼirSy z?1rY$f6@햌}HL;ul~M}Aml0h_!&Al[dV ll_G̝z\ sibvlGG*0vR,_d[ "B0SI.5R^Z۠cUt;;JG_*l裂@Hz2;(j}CIN!;kX 7=)cll[&2F'Yceǁa ˊg~ ImMVL!DHG[nU}u`,t8QkT[^pzZx}K"*CW1, ik JŌNϖCDm.Xľv%)2R"eLص3қom~dH&}k}BX8X葉M9l=BeJQ#{(eN̽}mϤ5SiAh#wb\\y8% NBCRu4NMH 3Zݠ'%^ς1d\&uJ:?uUyq3x*C- cQ #~j#:~{SR}fHnn: ݞ{2)ט`~G͒(6|kJ)fռ(Ҟ˽7LM%y_ /U@Z n==%֠BgI|y״_[ KYU_mBui %`i42T#Ņy8} X{or0YmN$=:*a#w=#]%B=S7mk/UM\PK ×U%%J9ҳ #%6e<]JvY+x])Jih*{QZ%~*4C[_&$jIJN_P^- Lh(ho&I$U쪶I^dVŔCQa0rt UYp:PYcQUr+\edRxԣ4Uf"BTאG"pAoxt%[LF%)|u<46PDjY+]JR*qcXn.Qdt 620Z4D$ IAI$>AAAAM   O3{^QQۂ PvvAAMZAA4bx   AAAD$   &AAAj"ٌhb`XP*84 N'x(  ?gTZZJGGt:RRR l6cX0͜;wFCzzs   |%$?C߰=IJ(?@ zGHz{{)((@3q._RRBss3sET=(l6Q =I zLSoo/G%;;{_&M"&&#G3?sC}{^ npƪUHJJB}7x<;qmuu5/". B믿N||<[n?FTO0k֬1W TUUa2 99!˝;wW^yI&#ٻw/yyy?' `?_$--y%>3/_>嫫y}ەڵkj?~'OG~{9j8c{msߓwgZ裏Zx^̙Cff&ر\AniTPPpCVKff&,\pԝ"/^իǤsr~. c^gvT~3t:aaa8q={SUUŇ~HVV*jLGyd2a25kUٳg)++cɣIII/aaa_qqqōjٖ~_3ydn7yyyॗ^lyy9;w'?/D"aʕ_/^륦ݎZ_qq1&L`ԩ{1~x:::8y$qqqC.:N&b|  JKK7=CŹs0au/عs'nf󣢢lz)xYx1'<<￟-[s=9vUVi&fxCzz:[l᭷… kXիW_Zͣ>ʆ 8y$jk2g:;;oΞ=+_oo/2n`x^IIIa…#^AnlldϞ=H}&.^(mۆVjb6뮻7nsUUU#kUUUAR$>>>X~=qc<,^={믿V%(( &`Zt ΥKHMMeΝ\p7|;ߦY_t)?|===8^y֬YÛof.~ӟcsN^'|¼yxɡǃ!88+W2gJJJе ;Tŋ.֭[ 7rt:t>rhkk㥗^BVGjj*s[nwtf͚av)..桇bݺuL0!xU6P.@SSmmm;6&7w]֭[Gpp0n{q`ZY`k׮6N<^{gϦZV+O<O<F!FIKKt9b ".'x\L&ӰYN~0w\r92+0jyaC>M>z}wY6"d+qw|6_Koq[N+b yNg ]Ss{}mUF 7-4L/<,d?Fr_L` **@'44LԩS7nj'b0 Պ$88R}YBCC D*"H 44*++%??Z:;;IJJô @bb";P__Ojj*ӦMcԩtwwi&8bbmƔ)S@DFTTv8RN'^;=^Df̘;6m7sܹs9|dCrr2x<oqi8s=7~J nJZZm#?K~N*2M˜h4x<nJjj*Ԁ0߹ߟ^>}:rӟ;ޭVuUVV&55'O5qn2HR|Á~yG>#tJMr^E8K^7̜=}'تJxЉ&%館؈!AN.jC4]Kb F^32α,*  r/D"KUU999XV jqQRRBcc#9uF׍?_~ٗqzڵΌD"aɤ$//ɄFϏLT*| f4]ϕٞ"""F㈈e˖C 6P\6ykwD¢Ehkk͛7|rbcc}3g>rcʔ)tuu1yd/_Zwo6^MCClܸoWѐH$L8x+++x^:;;&99yċS[[NCVX֯4h/ =tRy4o~ډ -QׂkFJ+FOE!Ӌé+(#J< -j[+_ K科2v5{`вUtQ4q%j\rg0l9vt)<9,Se)o즱[Y0+eY;N#yx^<phgG0317{{w/86;)ﰳd R,(dnoVglՠ78pKNWeWG4fxp.\)**g \p:tvvry*++ ~d2RSS9}4!!!DFFDaa! jcǎo @MM aۉ?dݺugV+ dffR]]ѣG9t2lH@Gpp/ue4ͨfS50c zӧСCC#&ޣqe{zz8vYfzUi`G":dCJAhٳİ`T*ؿVpŋ?cz=.]e<8d]&Nz˗s9d2n˅˾sh>E2N{H$kf?fՔWv.]Jaaa_cYr%ׯ_fڴid2p9y$G^ ))SNi&^xa[Gd2 Y&22gywy?H$&L~#_k<裼 1]D~~oRQ9m4vɆ ,] 7nD". 8w/C= 11bHIIw%$$ė9OMM?ta6g>#~_}CRVVF~~>]]]r;ƚ5kg/9,<͛ yvQ ~y(>V ~RkY{W,z@7hS >54Mڷf|uyݾ:y\T0ΰ "`( FmL$~צ߶iIoo5m&hbTq_paU7082,3e* kL<~sfg9cu|GlC1:D 4Kf#C9𪀳3 aڴ>@+*|yi]K65Gev=}X{K絁݌zvwNNQݤfK龼6??b7<8UYY_W͛Grr'-{!--wHtw^.\qؼy3+V_c5wFKK UUU 0ٳ=Wn{#O߾s-q0SjjUÍ;)ڰݱ>Mʧa3GCνφ_ "oɦ$;%!fk3:vÕfHI{+g>7SP~>#-f@~7ʴwMg`^O؏fD@Ld>2otuGnI#OޭkR&}2͗ޗq^MqSMnFll, .d'?_;~~~ߟM6SOn:ϟ/%G᭷'F/>2Mos@0KW"&ԟ'/iَ添/l=fץcSnohWϷ60;F 1]=CsG/wǥn!!~X(] Zg/1k%>u4ωӯRs|y3uW7ٻtᐏA]_Ab#/IP / WMHNNd_p{J!o@CD mߟYUG\?fD"?-'Tg@4zO;ŸסQEoly;L}؀FgWѸ "-+e™5?W#'?]"uQ<[gST~[;&he!, ú9um^ĜcǥM_-\9RGREV@H"|&x0ܗyV#G0z>ݻwpB!ny /nB? 7&H !W7yISZZJjj-理@ +ʘoh J/0 !-fRR:#F IDAT~) f&L (__1obbHN![rrr(..… 70#%(B!MLJJԩS󣼼E`` 'NDRΝ;l̚5e%RJaT*Ҟfٲe8Nj5jɓ'ñcػw/*xFuׯ۞ʡ;///?~< q{{;yyyرcqB!AScZ1q 4 3pD||-Nzz:[nE3l0ۇ/3f̠\.Jɓ'ٳgٵkQQQ(VΝ9so} ɻKBBgyee%,Yv;---|_GVuVq\DGGAyy9%%%a6:t(7W*9<;wۛcǎ1rHdbǎ;wg}???عs'J???IMMeРAlݺZIKKzۼy3ZAnn.{ŋ9s&`g鞗VWWs!&Nm9QVVbG!00ɣ>Zoii9sɓ'q=Myyy8|}}1LL4(--E͔)SPT\H*++jL6nˡk'|ooo֬YCuu5jqyy9AAAdggc6ٲe gϦ}/ !K4FջL& @PCNNn:hoL&T*T*FAjkk)))aܹx{{߱t  mt:X,n?1i$N}s:k,蠸`ihh`ĠT* %--pICiitŽOVގhdܸq%%%C]]g`Χ3'//SNlf֬Y8N{= /&L]vtSyտn'<<$pct.0tPJJJHOOf9sxn Kn7;v`PZZJAAǏt2l0BBB裏0L|( X,:u|X.0FEii)EEE7`vO^644Rزe ߿Z&BA߾}9r$Gܹstq  /VszS9t<ݎ7NzjucLtt4o999b{jL!MCO7V.||||4}tλp۷^oV СC2dHiW*RϿSOeAHHHyy8d;C>}0a. Ɔ P*9|2٤snpP(<5j6Ph4T*}n5 qqq]Y,Ο?OCC'?;::=fvӧOJ% 3sB'S|||P*hZJ%.&裏+_by jjjUtt4NJ%AAAkɓT*rss AѰ~z>`ٳg{jbbbFt؈N ш?\.N'6 Vm\]#z3۷/>(J .2QR?Sތ`vEx"UUUaZ- ZnSSSC߾}Q(x{{v1 h4nƷn>>>vj5tv`̙( /_Nxx8nqhh(555ٌNC8_!4UC aÆ nFM_~;xy Jbʔ)X"##o;F}tRn7C4s\lݺ\bcc=z47ndϞ=jNZ&##UVyEGGS__϶mhii?kXV6oތFtN&##իWB 77pF#˖- ::Z͐!CX~=+WkO7IIIXp$55a_#dӁ!_s&5j `jdaYZeij6t2Mebܰ-cբ_QHaCdaˢAU˷I|}&*P-|Hq$dX˪3(4$/ 4Y I!$hz~B_S3 ^?\&H̀RF>LD ߴybB3־ݓY7S++1BtL"mz*Ia 9fП*͢T~1K7OQj"F-X*(<e8"TTZcE=Jj '?cgvػJj(^ư=oKbx@; (d ^B4Y,***((( ??[OAAX,)1!W"{W9 ;dˎ[H?R||&Z-^`b`ZX,IJJB/)ł`_.72?DBTУPd-@''K/@vmFxx8Ff``0{e|ȣ6!_f6*6¾эNLBqנ={0r~Jdd$v"33'?6m#<ܹso;nݻwgֆn'((Yf[/\.?OP(Uj*>./X~x8p+sq . BѣGկ~Ejj*?"??u__n1~_Ar:Ϟ=Kaa!3f̸O<ɒ%K<5h |Iٷo>{aӦMt2ԯwnP(p8ߟqơRnyYFn7 :T 4Sk-kA?p:wR'_i۶m70u dСl۶'p~&OٳHxyy7,XILLd2;ݻ)**^`׮];׿FVߑiϞ=]QWWǨQ 8rC cJk~ׯ k2ӟķm dݺuEuYv-?я$`\7ŋ۷sn?W\\1b| 8V+yBJF!W3h:|0n=000=o~Xv-NZ#G /p ^}U&O̦Mf̙,]v^|E޽{Q(̚5ŋSUUExx8o|lذ$.]si?G}ٳg?Ny뭷8p:'|l RVVI_HH!!!n>3IJJ/ dddRj%$$ēlذ'O&,,˗Hcc#~v'N1_O8qMФhf͚5?;riii!++۷K/h<5MnW_}Gy-[///^{5{9 ~3ӧ9t7ؿ?(J ӧO۳]HOOv*SR\\̑#Gŋyo qWUVVi&J%JӧsE:Yd 4557 ؼy3N<\( &|}}=7G.Fn͛73n8oɶmp\\.xΦKu6]Nw˔J%'Oɓ]e^ ܾ~_XnL2e˖p83gNr:3w\}YLkkk6ˋXΟ?@CC3fL5jBi%%%; KKK!՚IMMeDEEqq*++;s̡FSO=3>>fSSSC߾}Q*z}Qgҥ۔sk?y79<ϟ̙3X,1FV|s%yf͚E~~>mmmRB!Xџjh4ޑ} ^kBn3pAPVQ՞w\6OЧP(jnN8Aff&jt: R]]O?ٹs'zs3p@^yOf]2} 0gpzV+./znn8zV?W6:H>}:?ϻ4Roޕ~e:?k+ &MDCC{̘1(NB )))qq֯_Ojjj˫\h42o޼?.áv#G%%%w=JF#:phll>MB!Xj iםӧOs1Okk w|jŋ9vǏϏ-[0|pT*ѧOBCCgƢ߿?gΜԩSݻ^{4ԩSTTT~<髪n{) RRR8}4c֭tcooo}ٳg=5۷o|7fp:k9rvillEPB@/5M掽`0Xr%dffɓ']@@:*MG;w.K,!--:>>I\\jD=7ӧO_t:x O~b ***=z4iii :=GKBٰa۷oGP0eʔ^k ~FuCC_IP0m4v}b3 _PT7<سg/24uSAӝjw3bccYp!} @zzg$88K~ !B/v}if1|>Cπ4K^456sCX[X۬4ٛ,ING !<5M]^z;ڈ+A! JOxqLm^5r&B @ !W}k)--%55SRRB``}i'vT6jŘc$S+@yDmm-.\0 4HJQ!DNNMaәM0]洱&~76 B o:pjhh)A!^%KH]x ˷KF !MwF!33RZZzC۔PXXHff5_Mq/q:,YK~~>'NBeM6} q׎p;& B/)Fĉ9z(wFb0NAAǎb兿?#F !!FtXr%VVKGGƍ#::}nnӧqݤ2rH 33#GpQf̘qG=۩vhuuuT*QTL:???ٿ?^^^2qDT*ܹƬYXĭ)--ETzIR1o޼/y;6nHKK L2ӧO' j:ĉ-2ʰX,<#0}tuk.n󐗗ĤI R4 L2Jʕ+V˴ihhhzy'f͚5TWWVMwyy9AAAdggc6ٲe gϦ}/}m/~CSB@ąiivɦw|C1d!. %ĀP(Nss3ZDDD* Jhl60w\X bj&tbXpObҤISύәIIIǩSl63k,N'ח &P[[ˮ]HJJ߿NRR]j8BCCyXti}||:t(%%%{M̜9s<7 ߿ۀgǎ=JKK)((`8N FHH}&ۚBA`` SNyrƨQ(--'vISff&*>۷/#Gѣþ};w.:.vvQQ ĄBH)r\ハ)͍sޅ f߾}jz:!CvRINN*kpp0O=iSA@EENpȐ!ٳwy>}0a\.6 6T*iiiSsd Uccf; ݎ@Pxj&j5mmmVh4T*s4j4X,Ο?OCC'?;::=fvӧOJ% 3sB'S|||P*hZJ%.&裏Zl8 XZ-7@l`,NG#BՐ!Cذa.n#Fss_#<)CC{wk9ވa\FިB2nЭVYtK'k`PT÷G~['!Kqyo4˓%(:̜9כq`'..~cX,=|fcFvUww{W뱏SfD&)Ob6,VXi^ea_cg;?skYVQT]tiX0f̔ \q"hZ/O<9jGB!xH{ÀtC<'3IiCOx~n],!vm[> gh> !Ľ%Y T*ϟ/%1i$Ʉ/fn%$(y"TJB@BÑ46zY<ҷtĎ%Y5;):'*koY޽؟BKR$w̡egv.#x(k̀LOBN&!j1 =y_R\hjۡΫ_PۯƳvJ$Ru8}֞Ը3>`^iR%"5MB!xfS =KCODzYAԸ7_0i"Ʊ/Nď x+F׆24ػ5)I!Biww;_~, =^ɪ[jh#x7ƑڮuHm浼U`Gڱ!$hB!E'XuAY&%nkg;_`W %R.B/y$B|a#~!6XζR_j@%d 7VbXȢ.貹C 3)$3x"voI!$hB!Iy,K%EخeoTu=aaL= g^ͅ+9*םB&!qU޷JҷNY@>dn43&B&!⋫_~׾agiػ<ԯ›¢[ Y8-}Rʓ *0-c>k`~`,YB? #~x:8~|Lra !$hB!("Dp|ڠipaWMiIg[`/ `j,DťsЏ_Ǡ6a Fde+3*A ~\j5m#V)<\:3: {P.FNuu{͂E\B/7=O!oP}׏1zK[lVeyNhk!vy Ya>he2B"E}E/5 _S^|aĖ1g.d_b~Dc.-C7f> " G!n4 !xɦkt:ywdŊj p\DEEqj{}'mذ\bbbϏ{^9ioV^ȑ#|͜9F#o111|lݺ!_g]]$[SSMMMdee]ON8Azz1j(*#1bĀ5O' ==֭[o~N*ls LŋYjՠfc޽X0>JKK6mڀWnfΜ);A!oBYY111#44h7n5gZZZزe ;H IDATNF.^?L>gy*֬YâEؽ{7,_?]w_|BGaڵl桇bǎ??3gOO<<@ӱj*^u9N//zADDn>TRSS6lYYYk>A}}=;v@iEw%44Vf3g&993_UUeVUU]4 |Wv ?s%rrrؿ?/SvYf wyyy?ϏW^y'_~{Kz!>Q*t^'dddxJ{8qv[v-< !n)n}G}O~~>gϞEP0rHfϞMAA͘f|AwoPPPÆ .>#f3\tuvMMM_ .0i$uBq[)]_p:{3aOGP}QVZJzg>|GT*psȎ;xjܹž}Xh;v ))5kJpp0ƍ8:;;1\.. ^?̙3)))o)J-ZDuuueuuuqFyWꢳzEKK otlݺ/^ƍyw$ϧu;y7y7Ί+Oyy9BHh^oVd2]s:???Ͽ* D3f ZZrԩSIIIAӑFbb"\xV:;; @HHIII(J piN:=܃h6FEee%F~n@RRǏgvٵk$%% h;utt`و`XVJÇHgg'n&))L>Cɒnh42ebcc싾T*1zhV+.sE^^?1 lD6nHAAÇ9wcƌ޽fbccQTT*bbb0DGG_u[kk+6m";;p<ym:DNNO>$K,aϞ=p5bT*W}B M`Xgnɯrx:\P(jj=n>BVv{Vt$''SRRB}}=`u][o{---3}tOn@P>"""K.nݛ۽gٺßLP`|AJ%o'tvKuuu8{, l2.\_Z.n/>ejs=p]w x]=BAdd$X~/D!z i 9s 'OôV9y줭'Or)cɨT*RRR(**bذaDEE1j(8ȑ#t=gri ^yϕӧOSYYÇp8zwFCCvΟ?^jhh`0 ѣG_kqqqdffrqn7Z(?t*v}zӶ_`0"%%媐{wFPx:6w\BBBؼy[Ztt4ϟtzߌ9d۶m̚5Wuxx8\.V׋v|~Z[[ikktk#V|߱`;m'>V濊xBoMB iM>)::M6QYYIvv6SL^N%Kꫯʕ+YnSLa֬Y^N'IIIjRRRصk|ҥ/xr1b{=*++1cSL{i2y:7'z~gy1cpfeܹرP(Xxq5L=OVVրIPd8p`3/ӦM4!#GpA^|EFѣGYv-/`kx7QT]u#zTT>,o qwwZeժUYDCܒFٳg_USSSgϞS=vXrrr(++7 44.L<-[Jb֬YHLlxl>m$#$nT!P)^Z|ܓh.*pBoMy]q?f3Gps/|'55S2eϽC/ .0g{{"##e!<ߥ}~] Xȋ/&Je͜5,~@ X)PG;ap9sl8?տH)jػW"‚:~6~5aP?O6r~oB`lV|9|;{K><_t ]K^Y O-'EcX<2,HY}03o添n]yy&xscEjqcsX.ǓB?V#4)z CwRw/-tKK *jHBSCC.Awn7BBB?~<;w> //r7nu-󻠵z:{EQqg$Z5/y 42gcCD)vq=y/dSP$4UTNJ]g*8}c_?qjʪ*(}Sh9*-Xk-M$RՕQ8 6*+);b4B5lx7l>g#?.voc0rh,䓂s\j&!NTPYz ԅLlf-iKV4Kk)KgMfNȘ?RBoۿ}BRwPr I}ݔy<$5̓?JP_3<#G ꫯʆB @IRՏ]ЅxU|LN9O ,ΒLzK,m&,sНȱSM FDɊ4Hпc%ӏ1|0z'y@srV/@d8UkG(l+- z1F5u:m㱹9/vV9i9iN *9aXTHB z=’R*+*/jxI$7LyV<~xpu҂dbر;+Ihxlbg' 8멬LMБ6:i1)I!RA=G=LYw?O^xW~2IMe"Cf<77&NHhhSII ;.j*V|'wO1&%:Μ6asH*r!IYg IQY-UǤ&'6-efՐA>5aKY2E贁/Ol3ɚWL]c3SuQXomZ8!7Ly3Whѣ hb;FvvUM}χ~8N֭[fˮ]*c2~o<"0g+QzֆnT\~FnOx٥Lղ7|FUT}[eM|B0]˛sRH`nK0 U96_㵿.4U|B1ns)//l6j f3vBCCI^aa!'Ol6Gpp0SN%99۷{ fdd0~x6mڄbAŜ9sHHHl޽{9s n &0}t ";;'NP^^βeˆ$nOmm-nT&Os&vڅRDRqDEEϏPϟJ|l6<-݅G}ĴiRyF؝h׮]$%%1zhϸNvɥKhlldŌ=_~%ܹsQ(^?l߾v{9N34 ! =7&[卌7łd*4 #Fuݝ>ddd޽{1 L4 CȲe˰rP*,ZxΝ;GAA(WWΝ={7=PRRƒ>8dcF?dyxILL#G͡Cx b˖-={^Oqq1>}eijj"//ϳ,X`Gh b…(JOcc#.8rrrرEGG5۷t:h4444xb"##),,-nw%..Htt4gnuV\.`֭[Gll,K.qVٻw/͸\.̤b0LL8Am+ϓ?'Od1N]]O=AAA֒R$((/2aƎ޽{ill %%)S{ٳVKgg' .$88~Çp8hkkch4!waҥS__ϗ_~RJKK1w}DDDtRqWPP@RRMMM:foNgg'F,X@DDhgŨT*6mD\\555hZ,YBKK<~8?8|ףV~+** c̙L&xG)--:B!߭Tpm23BA@@ݽkuEZ+FXT**HL&pJxqq1+WLXX۶mCףX,^Cl6vӟĂ jϩS )) ĨQ;w.:!**\l6*8+ b֭9ss#pq8~8̜9mFbb"J(L‘#GfذaxN@׮]oj5DFF2gbͥO?sbto9}4AAAL&yN'o6ƍ 00yHAAǏԶYSLL Ǐ'< ׯ5}@@'N O4LXs>f?3fHII ̝;ɤI?h4zP(b69}'|.h())*Svv6*A Ç3}t9v:t+Wؾ};UUU;@, +W|p8t܌Z96L$$$믓K{{;xV+fB!w%4 b׮]iJ3etp :t dbxO8~ˮT*ERk׮kpy6u;v,TVVt: 9x oÆ c޼y\.l6;v@Tr%vxwVɶ^pىBLjv;vZFAR1lذkQѐkl󴴴xgWWw]AAAl6n7Æ CTP(<㻧Q(y`P @TjQ*\.V+MMM|ᇞ㵣khr\\xYKHH^#???=r,GXX 444xUBBOJ%aaacڳZܕJg_bhoo0u_immn̙3پ};ngX_ux!BHhRSS]w_bܹ(J,X0tt:ijjb̙X,ƍw|=1 \r@厌ĉt:ZZ:Mee%t:T*HMM z:;;q:z6{(**L>^ϽK`` VVKSS\.n7L:9iWSZ ÁBwdo'AAAdggՅfߟOVVNVt:VHimmrt:lcXnx[]K >xRɅ |CT* l6{jB`SPPѣikk)S`X -VW;y$b-; AGG~~~X,^n vQ(v~hhh`јL&t:x!BHhFcv:u*Zƈ#᭷, Jbż{DDDw厌d_ĉ=M\.{e…y3f`Ν>={pIƌÄ =mݺ^ sZlѣGbѢEf-ZlvSWWGCC{~`YBshhh… ,Y222j~aBn4LII!< !u'ĭ˗lC>'n{B!=7{\B!BBR$"5MB!}mqȡ)t4Bq Т몡;]妅&W MBq 'N!MEXHl'hpu\h!r;D1! M8~1v4t>;ikm7L4[-؂RəN]fapNfFR * BS3IҞ!?(K6:BHǴd8)>T!251=ތk0b2cTbGX )oq‰fx:Qhg8tVT1|b\[p2d[XOQ#huzt. G"BI5SZa5QWL}$90g4 EX^LKT /dV3;]M´dgb`iIDATTñS*fgjhhVl̟draμLŒE_vq?gs&8wg8ƄhKAJrUSVg9f%oZB|S|4/[!E+ }^1գrl^hCN2pVu5] aA$N#h>c>-C+M?Et VZ@AvLKۑ<\8Uzlh?G;~D GA/2<.zT='hmI E*]~ V*p:mN jK WBpugrZE!2TEH!w$4] B|ᤧTdEzcJ{_ճ,+KRE;3w^2c47SWtenn赬JT=ɜq;8SPF[lrG@qv{]wW͒%+ ((opeB2SmY>4{vE!g0I=@s;n|7@BXzgS%XV3Oadfl=CʰpM48.|t3`ol "_pP L;BNW?ѵ`vN3FڭVڥ M=̠r WL!F)Cƹ4#I5Wd%jd N NB>ߐtЧ'er2}Ƨj@GdA.c!%wPd7*58!$%?G>ȑRħҙl*:T '424A4PpSKL&|EUIRv,ɹVn}2i9hԨY)>^&Gȡ)%BuW)}ǿߕA _](@!P1̈́ȆB1d E/}ٯ @'uep4VX}S\B!B\=ˠkL} LB!Chr3n?PhVPW!B!9 ހil<$B!zhrru3=5r'4]ro5M+/B!uy No.ǽ2u&+e !B!n78 8<)$L~ !B!z&oCޞkWue&uzL1oK!B!D?kuXcs=UɯJ>O),\g|J}&oHB!z N} w1[`RG HB!%owO޺sB!wl`#I!BqGn-#+BIENDB`icingadb-web-1.4.0/doc/res/icingadb-architecture.png000066400000000000000000015672211516124654200223370ustar00rootroot00000000000000PNG  IHDR 7eiCCPiccxڕлkSI*-Z R;VHKr')R\ׂmп@WYEΊ.Z )B CԌZ,v?.ëkmc&6aVW ̯O0e9^E8u>nL(|ATL}KeRGwqeGE*Vs#\q)Ze9YEg1㒎PkS즺y =UXUU^{SK s[8=v$yF+oڣ} uëw9[ TZ{&! ?SM1Wc_g) pHYs  IDATxyluqaxl1غN:duV}H@4 ܆@8D.[:7 ȩ"Jcc[7v= PwOOOx }OO ~m>-#mV-vWf\eqg hƟqOƅ~K+|Xx܈ c/?8}JN7'g~`Bl\ P369mo,>de(`z7.?Uˆ±Ua&1L9!q}҅xogQ׽J&e75LPrB7Όȏ?Coq)q?k _<1!1n_`Ÿ9[2o-+Ǭ Ӿ^-C<'m罴.5{ lx.}{̥]T`?Y+칿Ɲ t{ @ZB!"и[×O\ʾ^9pOd_?y {fǮn+HaN=-m%罥{<L褾ƟO-~G7ꖩ!w M{ݕ8.YُW=dCпc,n @9;ߤK]ޚ"NU-akCٞ?hq(Tho*-L@'a:T63}hO>2hsz7ōq6z~=ҫ o S^ ?şb*ԾGS+>f]: ^폦pSl Om S^OㆸQn}ᤵ} @wgN>3m-aׄ=+87ӾEq2}avk㹷hnxloN}cz" [{n00P4wiOƟ-qW4IG?JgVvΈg?9} ;;iYмtо3%a32'w(RNmYRh>̾-#[TmT~K*n~&Ԭ=lӌnGN᧋ݽ.?)UǟM8k](gI6~gzhz7O/Xqfϼ㊿n"`K׮ԝV5ǟ~x3|Qu >lOwwli2TW]UsxP2ӫbAwbɍ?gܲڷu+ ?|nmOlU\&~:ş.P6dϼj۸7IG5Nhzm~IOG2_}{  n{z؈nH(-牟z? _$'uJ'7T\Ϳ.1Sn,`?05᧻osM9+"Psf\vF}OO'Rj;iiVn0`Re#*NOEo^K,j [+|AWK'韴~B @ڸb70P>O7\ >)(7}[:S2n¤ß Sf'boդ3މ% 4ꮚ)oO7O6m)?q-x]uLK'v L_ٓ9h(-u)o]usr~&VIOu ?>q)SWfֶRJOߝO<6쉍q\. [s^h?Oj ws^p5r`sC~'ɟ~v]T)|x3yOd.P?gc@(m w,maƇnJ_MB)mf1ЩtBkd("(:% ,QAD4OWp}rxNN<'D)X};Ï}hnqLȕK[ve An @VN>s׮9/"x W3;|8=+ZL'sqllZn{},/:U?7M r:EWOx!CTIy>%MӼ/lwGy=z.'] @ 4<_?V˄|Nݟu- d9㏷hg^JH MήIhzO"g$`rEF65h5Sގq'6Qt]h3cWb[ c绛?/wrrBKecOMc7س܋ɢ>uKb7kVym=qRNqpִJekxhVm66Fdr>AAcRODBP+2 (QQ{l=3׵ֽw\&k ?~wLy~^g'~@;Goʄ .S݁ kmd~BJ Q%D_.sŏb>[w{c[VYr$P/@<'%zi (?զ g{WunW"GǦ=创+?vOTVG(c!y=@#xi|훦OiOo? ʠPh&#nvPE7M}@S.YsФ .Kc)hD?ZE{^Ϗ ɟxퟰV ]n9y䬓 ־Qb ~,ojuUӒ9謇ݠ?D$Pj/^g@*jٌ4$~$$Pc:ԫqR-?q ֏ӢIjX UծofozG$@7C@} T譴'5Ʋed2%}IgKμoҽdOV d*=1W}DHhZcS'> ^um ^(#Fdh[&.,@P6-Qp>If$&~RWC-*~l[?T_h9/Ϟ@QoIIGmQڿcєS Pܠ $6$TC[@W>a'@?Prd޻$tm ْ@??Xe'\շz Tp'?ahTwU| `*'͈ڷtO9Xf3Xfѻ)MKVY䏢fV)4T;kq/]|)@?&Ls@?:?>ЮH0?ʈI#fdhG?oj(@?JyT2޽_j%OVhZ\g癚!N2> P&߁ii%h4v?-6@l,1dORt}E9F I3e>_@uDI 7;K;@4?@\_66?iaQ dR&?6e!s >PAORȟiRΕ@ŭє_c P7 `1ᓕ?2t-@&}$2p\%6 v/>ϳ-3ߔR(%75")@ۦ\՟ߚJ>jXSϕ‡ٽTM +԰ly1$,&y۽sf\ O"UQ (O\Ih )=.| 8z l ֿ h@6gff%EО|`V ďPХK_oGN|ݰz{wovڤc)[rH?Jf(mG(I)z'?]D$_69*$&qϼUOe? ?# RGlh+|k7p+}9Y5~YQ4cGt*j$LQOT b) $! 49}M95I'\o@@?jEn͌9~)@O?({/| o[+%~ɟ`[.MI@A??@woI'OVv~U(]J 5RǤlR;'>H'@jԬ6p#UpA'O(aG))jJ$"cS(@O6ѵozvO5TGHH2æ?9{HpM52)@1ILmnKoy3O$1m䧢֏?*Z@&z$,"ߔPo[H?z}jC$LI{H#v@j(aGoI*[$^- w sIdȈQ?z[ۻtJY#c %P |l**l(5(lITJneשq\&l&np](pmGً@GOQGi n2SQS3neշ^r.?Qc~/(PLֿO]@9GI =>wךQ'-hLK# |z\*)X֠XGH'7p\ O  n(&{lZr&~ҩp???;M>nQef%b׿nVRG2үf}xP ɟ46B+ZQXu^=wÚm@?1$),7ES(%QocH:;|»?6%~MM~ǧg>RM 8zEGf֏E(*~|Z>: 5p޵=MW=~P ,jH%oEH颴#~n?z+w|[)MV-ߒ?{_tcg?ɟ@Lj&.+G#|FU?1dG WI@#mI4K 5ҩHi[un/^v| ;_p*Rhl(.J?>q $揽Mlo[\)>DXr%?6+Ydz8]Й&~)LOII<#@ @^PO$mܛFܿk.GOb竳7J;׽NN($pѻ??6K7$?KkRd)HhLծ)CȈ jZ3qO>\ߎjRȞN'Of0ЫnWܽ뷻 [vg޼ P &Gs}gs Uaӟι?]?qSτ귉@lOYO?ՖP)ֺW9a?a25DdWO khT6@?j(ZfQfP//h[[nنYPTؼcď>^\xf{V'%h%?6M(IqOqgͰOTxOx9ٛ?ٷ%?i' AEО|;/V47M%@@J[?}GOyѽ]7W[ݠ-? d撫wo}cBo?ďj/孀RҾcf OQ>Z\3~7T`@鬞<(C^}`Z@4뤧7 {[?&KR\=;wi.@AH7$}tG(np=[Mь+^&vlS,?z+3HflJ}M6$?9>afhOI4\YծS;R'. @NRn9= oSzH TH)K1%k~-7fˮvUٔ[&dE$bTpG HG͟ ֌Qf){ݚ9I:_9v^x';?j%~Ubƽ}s`@JcML|1>E_x1hx @33m-uڂhET -x#dN7.ҩ3'+ggFlחBx8!|Y{B!B!B ku\h;cdS@ZEI - ʓ7oȮy,} O &cgܾgknj^{X`Sa"he'OwB@?Nϰ?XS?wH͐@oAo1"L׼jrB!㾄B!оȞ"m"&V?vB`">hZZg)|p0#ãRDkFdT'wMudIwY^^fcCԦ- @h XV 7_ [A2+H.)8%Sʬy tʧ&l7y_tB!BXq_Ƈ@ ͇@!s?gψ@@^kT Z?Ⱥ`uKor2sCl=/ ' dCoe O'M'.#e'=@!wjuwL,b[7% }jecrKɬ9V%P@'7h͊hbCE_xB!BXq?B!P>bdJR@n9nع$+zr qm}{M2{/P^ F5 X6Ɠ_]+ 2-R$DgH־}XB ~L^cѭGied ׿iZ T&q" B!BڏH!q(:FD; . ~tH ˷ nP=GFl>:,7u׿ݟlbj?b|yY}/֫o-Z?6?CuXߴ\8VL 4HT){]fίߔɥk'ɋB!Bҏ'HR|B8@25sFZܴ%O5@paߦONk-gd918"+ ̚tcbePƊ;d:<$˳}2k$D% |0mܷG77QQ7n TuYRR[*lPZ}V㋟x> 1;OB!2~/qRd!2NndO?#Yl@A`bnujZ?=&{S)d.| yJՇ2AN-/DqcghIbvtA ir|{onS͟@(ͨ@kTB!BCKL|$s!2ؑz:Ҝ:Oq _gbߣxdeǠLU'l@KªOI[Ui.˓o)ܽ}7TʍYml?yIHE]&Oߴq NO?>??7-wL_;hˮ\ss{VsRoH !$! BGPg"Cy@P Ҽ4A*@$#M#M%T==w9ή T%XW7^gNqks] ?Lq,K38$IxR$I$IcdM.AA)UI$IE>}_:pJU!:. ե`e0Ug碵{l¦"U5[c|#,onjqɶ)%\v`ϋT07 bA;O=BJ,5*r9ǡ *,*> ve,> |oF H$$I$IDaî\\$I$y!pנ]FuQjN :@KCBh仏N+-g.!H<|,ǻlaUrՕ+x)4Ԅ{8=Yx9F8eƭH|r‹ JucsjYG-k Vv6^| ~Lb0a-^řK׼?$IRJ$I䈢 ːsr.A$I<(3ը "(.hDoTxgw*ܴusn_?kED+b2gʞ_կmiK{ Vpњ1ċ?[}˛FyIiYZ9z`a )*~U0ǐGԷ#!64xjQ{Tlr?{&{~YLY=e(Ur3B@I$I @I$I)Kۿ-B$I$ɓgz='V lREPgPP-"^ =+^WQ*y熝<昅|5kٓ.|x$Z9T}{xlo/>t)[ănaHAۏwh=g/9ff"u2Q_bGؽE}#!51H;6]xB`|?8jYAQ@SՄv;g.:`3tT,9{Ay±\:J5*{NE\Yۗ2zhWa5m9r%3'I$$I$Iwͳ X $I$G63O|"*qBzubZpgX\Mc<:߾`ej\Eŋ@mm/|y+=Mn8zF$**! V/n]րm>|@<{U?uBw->r#nVXК>A*sԪBDqE,;ݿu DŽy3A7̈@6I$I$L?pv.C†] \$I$9ş)] msoRzˉ=pXYkl[0V?]*>.M+Vg/ A}' DBTŞuxw?33˖rβA]oF6(P(V/)hRuP=S|g8x0>a zxK""XBL_ELq$F_珿)!,̙LNd՟$IP$I$3'aע\<ù I$Ird"'3sXBTPM)ĝS!X#(O_W_;( ~dsJ\}xBEjzln +Gl V-.x·G7t9Zs ] n, 9q4rGl-֏kXCąGKX(/V.?B.V?gD$I(I$I.˭yO$gXǰ— F4nj6Q7 uI]ح#3Rh 4BTTB"QblB}hһwOkO_U/\#,W[8RUMڭm,on ܵ9G eH }=lQQzldN%ڰjuq ,<{4>#ʟ"X_S[B8g0qz*CDV#򹑕ɯ$IRJ$Ip"6:!I/\ H$9zѦC?:.|L05 6V D8`3AR#L1؆\{6ϗ;z_Q.Uh ]>У- &NmtrԊ;#JeNax`V>|vB%5܅Dn dG#,kʵolgE rH/ - Q\vM{s܂n7USa3)B^ϩk+BS׫DqXµx9+Qym|<9񋎅 uow( z`~5%IP$I$3aI OZ9)$I<)%C^†]Y2:̋1˦cE5 hS8wbqB8ޕӮebn tHY 5\?v euU=R#2qAu}ZB(,/>pv3wW-O_ȼnΖkCI4Q~pE-4$jmϻ<2jְFdW0~Og ѝ")Ջ0n7vGvLRs 9Lĉ屪*V@Qlz!Ա,hUس}]M jX0R>|^Gk/ZK|Uv Ǹq'-}t7Д+ X'#ݓ|νkw?<w]M;VV-jqBfŠV.Z.  ` !D䚇G9ea:vkk"u`8sDk)'AфZw F6sr_s^(T &$ac"\wf\@WT$\$I$Ic/aîȢ4;URkri̤|$I. ^].xb7N6M]H~ Zq"v_u>#GxJԅQ5ѧ,(*w (Ѫ(6@q=JW]J$I$Igz+;?Y*Py) ' MXje>p!,_$Ieඉ(:[ =a+21 Zvyvt uq:,![~pD<-@@̥}!@q UPD1Ýc2{+g-Yw/nC[h4 T! b l^HT88UWWZV,(*joȎW8KCMe  w}Ӝ;fvMrIJ=C%~Ց߿>j1x^尿g8їI-{q _SIP$I$G d(Eg 4Va~ggyvS$S҉/wbD&8$hStp.%pcH8?W+)esXWQVŒE5kE+ UTe7vΙbx46[7>wwOGϯ#_pv (Nz@ђj>Ov}ctm sV@ ۭ݊ 6^yoS[gaz|k6h܀ uG\E.>MDxjIգrm$$I$I#AtcTi!s#ٵ0I$y@v8p"qF]?qk/cv#.S;xx,] ټGwQ Jt 9GPYB@J;Q`O!{= >T A]nEg "T7̩7;:j%{;o8qGJy@B !ޮKB:>_A~sEP %:w"@Y(pI^|z~Ŭl:PDY1AQobViUong|e!zTD@@DEP]%-> T{Vro{m9K{y 9~~7?䈵Rk[|ot> 9KD‰lj{C-i?")ĕ:&6S1'9jş*>oί$I(I$IH8y 1E%N]UR'c>ppf'+>^$_$Itb,b 9F5Ph6*PTpk4|9;m>]*s),l4bO{_m猑%<>zkx-%*sunigcm`m4>qY qC0Y*/^;zh6QY-͉Sq/ 1;F!q?6r)J$I$IT)#,NV\k9 oV+sY$I}#NqևJ;Vo|к-l7ߘC; ]t߾Wf;E4(b\?=1*';'W;-_u vhU'4E(Q E{>E^yۆ}&WO_ E0-`pgq)Y+m "m%˯l-A H3{П߶EY7Ъ&E$[{ϳ{W7caO7{۽]DBD:;8icϙ qLUB4l7 `^63טNj"" F)ӱ$)%I$Iˁ|X< 8XKCѨğ3JP[%$IG9b:<^hx1C xbä5NljE~^K;~A ؏E =jmUHp!Z9jl T/Q!n~t@bGq8W@6T0j9V;?ㆭ|1~xΊ)T{e˒1iR\}"!N^؍uwYO.aM6,U&\u~(뇺xuDBPBKioã->?36,眑zBr_{GADn;gH5rH{^`CY'L +;8+$$I$I#;f0TyFsyt8OO%g)%I$i3%I>t-N.vyGj+nۯ `RB% 9=&ЈUxʼn66k߶ϖ: \)4U (F ZT{7Eǯd6@Pm̾6{~A w (6\`}b[Jykg5+` e-%_4{la87G 4j%`wDEٺVs_n)E&•gS$I$IrU 몼Ccp|%],ο AϨ`X[1I$RV6$SW % $r:o]ׇH{7n{Q`*/ Ppl*U{7=Uh-X[bK0-2kPBh&@@(y*aXѩĩ{'uoEݼ%4ʷ5wZȊ,,߱W98xv0{>_ݱixÉ\BwެQ 4Vn1mor~5l೎ ִ'_\!BHQYPSX>V|.:)IRJ$I$9\VV|O_5ι4`Sʽs>\ `9uy RJ$yʻϺk"NQkԞPU njGECN7n{<6ijw%?.AL܉=AVTD-* sR 珷WPT(N_CwQTJSmvIP@LO;[^ X1܌Tdr j 8[Lg3 bI,C/@JA>~>V 4yÜI!s2>7S2U?fb P\]rmįb8SН?R_> sl!\lݟο'I @I$I$= ` 0OJ9FyrQK녀<kJK!(I_f_B 0`R6J`KwmX:yZ<+uǹw}SZku@=P?zssq:Xc}oq jSQ 7 b5CUPUڪ%(oU r%t ܱk77=5MPq̭^@, S(}_2ٳv hZ80v~ }1"Aƈ*Fu~m,<KCYxRV6Y?`_DDUA v(90G%W붎ңyً惔]C=&o6梥}:S%^ Yj=Kj_'o*Uqq朅e} utpaAr ŮACa($3$I$I~ X r$"tϤγE$bP*/¶vM&v6|,r\8w Nŭ/m|{%2<(ǟ&Qe KuU'٤6V6JEd.?  *nO?µ?={ Kϋ~5Qd^s/m x9kI/ J P?X47{`W1U ̈́ BgNdE?,覷i1"U\aX TΈ%6=G,)</yTj] hNKG+)IRJ$I$q  xJk|q~d&] )0e ԕL 6gSqF~q?؞Ɨo|lyW(B/SphD햭9w(cX >U6PjQb1ݨ; Q+UPZ$v;xp\ ܩ 7(8G~$/X$I$IaW7H:V?yӜI6mz\=Ss2yp'~`~~ qP1svr:? ںJ]PQܹiacٵ[R%xtSF4A1 ll;Bp scLPm|8y8b傏kHjSvX0˜k]>UnA,sn?7miK{yIS qoqex @V!P%?HR{MهF9*y~XCW!\;좇K*!ǹ|΅A#9B$@:$$I$I'D< HYO,=9(6:v&$.n>,og Z C4+ѽrƄCNS vk1q@tA90l|_7(ӭMS¬ Uv/359\ۜll 2++/mJO\+6n@‡}x=U7/A3Vֽ_U!6m-D)DQoO=\̱QqCۯnQJL{$g~g6SRƝ|qŬn › Q 秀  AHY禪WՎ[%R*HW$$I$IWt?)v[P''P81E$ ?gҿ_NMҙms6-Y p#pa~: L>k$6+(MHj| _iL''h CVDD,wN6Ū9ZF!v]gu$[X Z͝BJ,:\F@b}Z !Ed o V0R>t^~?qO}sקJv3dnv-c S糠qH"Q+(vMz¯4g,pǨ=-݁o<6]\ަ. @ 3Qy4m`, إEM<2k'BS2+dpe$I(I$INGأJ]Ǟ#G(8E$JI6/1'(͕ 9r] O+L6m)q5 _&r-qm_}WQ=v#:EMp.fs /v 8QQcCz׏mMg?d[?ń,;k rKՀy=Y)!* U.xl(Wmϻ_/Zª&\gqeX TSP*/<4ʛD[yg]A"N|zzۄ;yBv-.Uoum[4+w jmJ@iT"V.97&`Q 8'h4g6=+ r{VUPOGM_6Cԉjy.ճ,e1wl9ʣ,?uʕ\yE6Ryp4{n;9hFF9#!@ $Mpx?c?&` [  (PYܹߝ^nպg@B~s`t@Ktɖ2>mt<N-u*ݵN~>/D۵yAݭڼ w#ksADfM䢐H/=ʱy ?sOӟ..`-'JKuΤ[8ڀ H)I%G(h]Ak)`|kO[-MV@5FD w e֏*>CP$S#DfYj ,@- S-&U&:_].d}4 `(p?3K/^ʊᑱnRp)glqR܆S*ۧ{^KjRI]((h6)0GYN_Y۬PzZ_O g bxk C+]0Bx1$?bSPO I .:B s ^}h"(E"H$<{}̾b{8- (eb/GzQJyw ! [؇D]>8 ( 8(yb*,7 ^ (O~t %;wfJ:ydy{g? F  tZ~dei9UD>1'k$H$D"9 XSI'Kh*kE rå9snws &"qͣu"Pګ8E7wA]Nܡ' }gtjs8TNR/L!N "49wOއTȭ\,YC׭Uի2թz\A8+*y?>l?e]r;WߢJ۾U%L')r JGhrHQmY]:-ּ_9|L/΄[ [<.(>CaZ9#Ts3AV RPf{8JҊ !aEPؑa)GgW_? \Ӌ.?G.N, . HrkJtsu~N|_r8XCGT4_GZ;0d-HEܣ;iڂ燂6H_.VS1-EXTQCEOiZNw-~.GٴoX=r )u~CoxaP,Hn_w0>u 8H 8d=bPI)WigXLxٺ&k דZwbJ]kXzi`Iʲ--P-Æ{{."z՘CE|}Lgc Py/&! '4@H"H$DOvZ lf\*6X6ȌԹpzyDstڃf1;:Z7&fgcDZb\byǹ;ñ95}86̵ZJǒsۮ[__(u! VXE8!Kq4U_GiWPL$ A˸rliEOu.~É&& Hyr)p `\=MZEmOCxoS'f%so FEm)8ɇHq"HfCG"Qo#sҸ Wa^?vO4(ǝtOB(Az2&zݼ}E|5k9gZRv{⮱O[ʯu  s">2ͻogI# u+0Ѐc7i_Q`'Kݱ.-,o$IcE VI; ~AE"q-ceNF#Ȗ *nOwB(̣T |E0[ );%HHLD"HVC<٤Vդӫ OG`QQrHU04$#Q%r8(A3C YB`QTad15T1IFڌl.vt8$IZcQ(F,&$Hb1y1zbX:֠"dYE T4 0ƪ( ݪzUjg3W1L#rு_~gt> ?c@/~Zs!aS'HneOjVmbd?فQwZ'(xbhºc|VAT3S x]% ϷvhGxE+jG;gRQEK?W=3>U|I P,EZkg|I,#8w_|d&{ 09!M~]qk<4rÁ6j}Z DX0 V`-dg'  $$g-C`o> e4َYcf#46 ?{B0N E4ŋ`ZLRLhoqQDD"H$r,xi*2٭!*djD@YZUHRljelfr(Lomⶇ۫zVa ImZtG!9T0 GztCQ'dndIFXLM$̈́T `1FQILwҪ6*)X24~s+8jo(LƜ.da 7K'9,,SD;H @0 wE~(J1ř @GiA׌{kދGR\=Y){3~ X q%,E$p3 Y5Dާ~qCB>%FE ~.>v$_/fc!M~ {(Ҽq,O\;'a_r4ؕhqtD69+1#?+`F[( o΍  " -;?e ^a[;5-cxY(oX*c4Q*[,\{Xt3?>7w9iI5?k-4XwkW徉.>o9?uCY].uBټ> x!V6.X5LYP0ޭ[3 H6:s6wxJJsТS[x `w4&8) tz>Es6y}[ǡ[&l]y*dm1B ҹySE"QD"H$9bV~f\rk%ߦi AbE5,0"VɣW%wLoB47Sy.#F510x2"/:2D;C{ *v^K ދUWGIk59ԮmO<+"t^|ץ>[F g7=N(owBڛ>NRwk>Lv7|6W~tzjp۪ S)9|G.(x()>?Tp]x 9ٺ s"g[~5j ~Qql=oNP~h]-O '.S@;˷O[βP@ +PJO7o~kK`$P+;m(B2>4wytiQh B Pz>rh4P! Z=޳zSD:j/r9o0R uw}`Ӭ g.ŜUC[G@E D(x?Vlf$H$D"#fՓRgL.AUħ]?̧{sE.i-}!3J5OWx1iBZr(uP"3(۫\RC;pLtd X#Oq.> \B?s6p-Y3i{2r<[Yf$a9/_LξGD;p$H0 sn(̸D#A{fPn$$3"rpx0?s*SuO橻4f^Y x"t ߦS;&/@U«Fؿ W77ޛuss’Zx]DJ]#i_+<;S6hMCM-go6%ΐ#JM#y3&_Mޅri8logF:KWcNYUS2z߆u W"'/)K^B7_XOw k⿗DD"H$rDHޙvY4(b@2BOڛ\iHR{ ҟZc$u9rq-|z[*5,qn-CD #"F O,㋋6 N 1<$DDP4L/DD"HiCd&;1i6AU֠6+CV!.b,K_i/!K_Bcp Ŀ|B9j WS)_TXtj Qu\S\8U8y27hП֌Kb ӃߖB/RrV?5?|tWC)bCgM ]6b- x$m}  gCpŒZyXШ`^ :C])js(% )qh l/z a.XqEU<.P$D"(O4i&sjXN+Xy$ U,U Dĭw.m?У؄{6ekY\3,'t%1 ,z4&H7p+L,./z*B-Yb .t,!2R<9eЬǻ)7g d9g) WqBJ˅fECڽiOQDD"H$rDߚ@'^Ee]#rԥk MuOz^tI{/``h&IK{,8^[pNSs u4u\>!3=?1D/~VſΥ?{n+xDy O^쯯~dZ%a8 % nB.͊_|;>D=DfalhE-Q\,Hط%p޿=?ͿZ•gt*r| tn>5vy28#L i=R4SJLH.6dEWi0R쟢`:0dYwL@+ zŽ[YЧ)KJuM8a$B),O g؋"=T'z8}p W F*nN 84J>H$ @H$D7y"o5%ҳ ֊XD2kĂ2C%`Eft"sgO~nMZ??PD7Q]@}\, A i7E ]Qa8ifl"ָld6ɾA\Yl> $;Wg?w5> &f PD\{?|})=k \ B/")qyx׍ D#!$)RIrM~C"A=abxzU=O݊aylU_8yű#/;yV̏mMpH"Xz-G;zf JA ߉w%%wqGLy'gް6J##LVthK=)x~b|J\@r"4'[:wƸ~ub|" c06.mp`ke ' r:HY= w4E?a,r X)b0\Y7G=^{AdA\9jR8^=G;#Ba^Y><~}xza$o}3e)r*严ȺI\;ǯ1R.(5 */ ."0].k 74'MH@wA</_o?8k)~2%BSA UY\ӗ򲣚TP]5%S8Ե3r#U~r0Sm%+STHe֞H{_ [IjVtnE%uJQW=SXX9}•ZoE,f!/rFK_m) KE A(P$D"ȓȬÇxYHM$Gb L̈́UKr7ע"4_|?JzLZ䵁"Pjd6p"г5JtEFIׁGӟ;W|B~8 xU$fJ$*O]qLGD /?Qb*-O5R8nxoS#wY"v?œ5E*(yά"+s{ك ߁ۃh}<@(q;!*Zx/!  ­; ;xc|k8~i wS+3vc&%5޴e_o$wHCO׷uq}(,#e:"P(w;P|/ np&`j0p̉Di DJH(_ZT D1/^9}ԻBR}lٞ%\F2v0ռ6$t)lA "1%\$H$D"2m"~Ms.u}5U__[uo+N9Tjj7fmD։@:Zki=xUɷ,}3C<~g!xsK8Ҹ?P~x9p79p -.3T-돫6&>(= U D@-W0C @saZQ4Rc"_RQUue΍@`y:<D^|Z+_{2Dp.ŒGH GKSi>{DpE}wܫ_ΒFbcɻgz~ƻi.^?H=a?Ǡ\`z3>48į5J{?ar)p֌f|i&{ V SR+Y|8R^;n 3)dū-#`$c:E?@v~2/Y#1@+SkdX,SHR@-!"(E"H$,L@vLȡV !`]?hfҼy>[=J ?ȯ+loa{qF^{{~\:84CԊeeՐCd04įi~sV>WdǺmWId7CbVoѸ|R`]07>/Er׎<'xpmi|*8;%EQ(# iJDWu Ur,`էc~| uYCLٮ9s-l / H̲,l>e޼P#A?NT (yQҦAE%Bj8')IJ }n;57^=aS)RT?}d{nnW䮠2'-vX Nty yݦ!kۻwޕi4trx+0xTPI›l*<|kBSxL^ kaN:%ڮCj)P5t5j _~sz!Y $l\Y=z:%z?>#&L Ӿ9'P$D"u15"[àu;~HmvAYjr ]W={,ڋ5K?{>.o.GX#Nq@6W @![ԮmNj @}jqm).n,?NuΟO =X&ނbg J nRf?j>Jqk?kn/y?#?ßځ#>CN~N%)Ăq3^3 / ?WkWK(*@~ dZ}|> Wia~_7ARIaQ2'/5/k?: ^D Q m~}\vYKSl | p 2;gy-wOZmK tQDž] \sƔa @nkigJ@3*x`t[DgzGJ楔hpUIB y0R y!i7MRYA\9w W9nE/ :nJ(QD"?#H$9W |ɎbUgj$ 5bUazvAjbş,̧}vsqC'a~(`[E9UηזY(~k~<At~bPm` J@63s0Tj3f擝Og[l DPI~/_??|Zp :ӟQ =Iȑǿ?~94GƲ!irguC@pT \CA7Ņx7B(s_舒DHQRt–j{wǏ'ԂCI>S~&Aoױ>GwJ1 0"`7o?{_l&-Oi"EL{M{ ~ajPDN?%-]˧Lr:~6 W*p= P 1p܉=)Ѯ{YH U(]V8Ԁ9t,cn!AU$'.)BNρ,cޙI;&{j?k+ɣh ((P$D"ú4,y/TE2M@MC1 NEss*B͕G&@( |u4r`h| m~M (9~Z2O ĞPkwdcHz ܪ椸pbbEWF(~F/;Wџiw/BGOCnu5^:bZ<-Q:=׾@8A /J[@P jĠ.B'D@}n.sMCο5>c)CUhڰs(d;{%\z|~mUrꑙ?wx g-r'ITX2jYx tT,0`&5 E2.x'>tr/1heసoF.Zl)"Ua^ ̖Z$O wTl1-6ǯo}DD"H$A6l VCť2Z#Ӎ`MAI{^iV?JA?,NQ ǒa}Y$T$h Ǿ # ~%)X }?Hv8#l#M+NJrȥ.]R%EH]-(ΐ@IUF t*>Q'>0C^ KwUs%K_ U \:֒,0b!GET]0-SX)OqC΋JXcO)8IZ)w:I^8PBrp't0 (BuB)@9C0ݳ;c+X?\+?3D"Tp O#wMcyY# (#0pX?TWǍCk4)7` RBYj |XA +-O xp/񿓟zoO"pj (:{UQ \0wίY^]{s 7.|ʷb_il'S0~\YtZ=pס$ /rϹzŃEI5A234+:Ͷ  B_S Zam'At\ }d[;9Oo]'h hK9]o\:љŌTJJ.B4lfݓ]  oٶ3L)λ*̇a-}s`xԌ|*ף1g/rbȅl,T/G^ b {^t,!"} ""傧v](P$D"D73o8NZi M*YU6&v^?i2/t˨+t-d }| h.g=EQH. A pkq̷I sk@Id'u ԮlL$3"][Ogm2Eg\| 8W.'<87.SC|%5hJR ^;WXL/'e|v+,3zsn UlI ߗwlXf-Ӿ Pmpa}#?Xǘ ].\8ԯ5 BGɚ 2ݦ<͕G<@;K*u`pR"ܔQ|{9 Bww\k'_xh~r}rVVoGBZNkb})#DCk`򙇦#3/;QÕA%7in~|[3k@)eW+ NHJ-Z&l E5E2}0%DV`^Ja~c@5 5%<$ѷk!DD"H$1'-SEXM4T ֚|8tJ,3aVş0&9S,Z$U!ZjB AG[L!Da '‘*V.%uiTp6Kd@VPD5K|Os.  |*3g??g9'uq?gD4DQ(vׯ~.>RΪE13Yڢh2\8z@!!/P/xM1YXςV~nVPѡk(<)e#GA($nNeO(RH7Da+h"J 4 ׬,+suȀO3ŏ~y'㝌Oj~E4% TvxwN8{x(!I`mʅxh?;;^~WdQEX{z=ATPxLhv[M33=-p 0JHE0\Y ޱ_s{b/zAd0;y*dtW e6tSv>"QIZD(D"H wf5Zk X+v]s\TOϧ}# OV_ӱ`!RW0E#AΠ A> -YgqLG r~{y];0NbQĉIr-r"#s`~& |С?or % ?͹x'܍ȏCIP(懲^),.]Űt)dZ(`$s )<&KX P^WH c(X|- UQK=YXR$*婯OVV ZSGji JX_EJ/@6и`H J! eUv]{{7c,pA)@ARc;oˏp̢*o;e k㨴%<(Gc>pөnj jnW hoUv8mV5 g6Hdq fw\)Jμp%V}d=S`9u ;[#\*wEDvSX(P$D"spyf@HXM$*sQ%1֐e^Ʌn̞G>H ұH,* Z]5?֔?>2:D}` :TpN%ՎVd֠M n{y`|Q|)?gW9:sB #qL;Axџ&g]şϞX7bOPE@$tX&Z3;Ar꒱1* w7iҬ5l/~ ]= hymM)\%S%Tڈ@W=_?whǡk_G.B&g!(α.I5(JSD{@XꉋMN]F)k&zy}惹5PJ(o(j)w⡩p(o<`ŀ)\B0ZVY>*sB/! SSu= c{* y%ZA u0BBy >{99r(4Gwˡ ^1[$H$D"ǍHn޻L+yX &F3~6'sT^\,(#UAkڰ0r"Mt pk׽l_INԶ 0 iV!>Oɷw,L \Y5 |x>ppU\$^ J6.Nx1'| :/\jxZ%V!3:ME3޽:G?ښ>ՏG!nEr A *?`0@"~ `N_a ?;'u )6BY8 Ԅy38ЀBC+k%ՇVe>0 ,g'7PY?Hdo \;߾QFJ mPx-S?揿;O\ʋVP^24M-џt2i.u~|0+ wQAѢ0a-w{=Ok6ѳ@06%OJk0l>ua&KW1` MCd悕#`)ۀҴ^dSa!.D"QD"H$y\t++BQ &OuBş^.$d`:m4ה~` '6#老JFІ m/ egͻ4 :7Pm]E`ZZ!5;OSEDO}. 0U|s9p:?1BF|w9#(^Un[^{şd#`d^yic'UGG0Rt(k'pjqBхHmސW:볬/Ǥ̣W>C7E0  }Ll(Gp4/\5:ʖE5`c}T}`綯>j39&x*"*Lݦ V& Evn읇 bFAN\L^gect{DR -ùĪ |Ĉ@DgN.H$ @H$D"9UŠ6CwUhn)udSPa)R˅ Ʌ6 ğm6V흙7wxq#E,m$"3vCSX3KA=+8^P ؁9} rRB焔M6p*!SE"QD"H$~\}j@1dZE5f&Jy)63cT!%t;OG\?- Q˜G, FI"k?`@@{؁5̴O%~\y!(KX\d[ڄ\Lr@I?|sUfs7rUq# 0&pp  "._9G;P .bzz}Uk{> 'őpIw b )DѢH~ M[PG?k8&< u`0 @E1 yZ/Ps@`L߯^ Pm!Bj`vu $0\EBE! zOмzoDu*iS(T şx9W77»#>Lze=-y;8y>IbXqZrٞ_z4*Bw԰R~0 HIx "3WEv #',)}X X,#_jpxCʅPT- H,cD(D"H{#q*ydb V j,PՕ ПG 1#TpHO1Sբ.`FtaPĺwY0D ݼ.LeG]?ARPN_},f)gt.~8$`+P4|/`k\>rooRT LjEJ!ű ^ 8}sVm?>MXkoʝ-@Xrn&˝o4mm0TLɽi?׮7ner .kHSR*@pH kk)wg c=轓`5•:IFܗ#S T u99, QA OR j>H$ @H$DO+$= YV%Td2=snz=zĹ< M+t;Po.L:׏:PILaD@kJZ#F0㘘\@?/t;IviWr@i]\}S\gs!p3(&p'.NP'@Y N 9WsW5 ( %iBXBUXB^hlu\6aCe~ɾTCP_HoV D'.e\":‰޶E(8{|\ALkUa܆1.+ (29.G: "ʼ|Q ,uPGُ̣v-C4߰K*%O2uAƃ\n{rNYZaawTtDqnǎ(KWBO)05-8gxAWI2 [B$\3>ȒE+1g,FRnre{]-".C$xNJ L%} V3GA U@)G(0nu!*@H$ @H$Do@k y 6{MkrO\@s# 2!Dܱuvؠ&~& M##"d=4uu҄^Bk@4˪bm#Qk@P$Ѹ:g\|:pyx-p!pk\}N9,>\z)(+X-he]܏E #gSyqf<NTVLJo7xqS y# dvBQ焖PAdb}X{jѹs֒{Cy"ֆbHi?ȭkuQv]kUpe~ A8vF[aQM=oR5+da;'D3FiF*K4RDqy~4ybqRV5+"P!|oL᪽-CTcF8vJ5ئG(BM2{[l5MV5B)&EFHz0#DV >(@B9z : p%PKR .SBKj"(E"H$cS5?TZP ;c .lj?NevT'"f:CZJwؑ*Qyu9Ɋ ЩA. kN;^t.B1Vkw>O6j?+x7p'j3> |g'dLg-پC_.E ?mlƼ~"D޴)8nQdA&t<4ab"Puy̦ T^J̉ǁj1=vqs#SdU"ǐc ^!uCǎAm-*},h0@-)֊E'pXuog1Q0P;c4*[FZ+ܣd)"꣐ D92Ep*׮Ǟzr~aU}>QA 2Y> yx卄l]Ķ5Ĕ`EsP 9ܥ ^p"a-y_raM9ba.\(O7(X{^qZ A΅ ]t@d@.)a9g)@u'60 DD"H$ҏ;H'’ɷ]S5yusv.hR12)\["m&L"Ea$aF#r׭TolOsF]g (FzQj{~\p/@|  hU<(VY:.ρ!ʜD”aPL@9A#sg_3J1k|1UB:2"T7/?-OQ*/{) CΗw6QE :` g޶ݏ_]bP5&O H{`<ֆie$BѰ(RGNFgNVF}Lz^ԡU n= BXwйt7: Yՠڣx%2t)|[@|Lf}7.5 :,u(R櫻fxM<?Rg(=sWTh'/Q5ҷ  AчA)Ҭ g-E(R}t{^iP@#!9eIaqSjW8`S1pN RT߇^o4E"QD"H$<*.*`3!%d\^O2LdAA(h3 yv `PvL2LlԹz}t$כBd)\|XEܵ? +y i9A8pSy?KP= S|PLXf`Y*u*yہqooaG6ȇ/J,cӠyaºqHf( 9*/{2,䑺7 2Cp:,^+`}d hNg[;-omq_ ؕ0Z84 '=Rr Z@+avbth}~;zQ'Y٠G~Hf36U `TO]d\~HD]wrsWmZ/^JpY,)E;u{&yp@Xۤ9(^qQ$sU؀BYnp6U``!&$nnpbo~sz,QBa72 }mY\E 9e 4O:{,H)͜(Z+AP@(P$D"Bޙig#?PN^v#uԐu2k 91wd hy:vqt<+:h7i} Fz pf˪7JyLn߿:.3JY7Τ?W04bt@]t3VCB]=5~JӻaG_g]]ɏﻈswW{?}{lӏ/#.0JͶmT_*̱ǂGjqn* "~`T1ցŧ}{0MW3/bxa̋Tn)H(I{ċ\29Q^ET!GL9j`bqHՐ:Sô}Y*Ǎ0 TO#Hwtz2~|!PS ͵*?wX߹~?Wn['- SK_˰? *Rt ʽm>p$U#ʖ^V=g\Er69ᘡ*/Z`iPDҗ٩йwlJxlļdrg2;c`:ņ_ȋV>).[z!;a*QxV* NQ@6?v$H$D"{p% .TUW쉣{2! Y:ɋw:f4 % vk<ѦXhMhfz ;~h.ilGCUs8 ?#Cx#]'^9`}p^+ Q=%Sb.T5_𰮍1ɑ{%D-tAQْ6oS/vnWӠ^ౠ].ou)gAwΉ$+u[@ٸһ)zEE}9SX^8{>s[u)-I .QYEfK)H"£^7fNH|:^xzx.yQvR?ʦ!BlJ{ha;vRx5WŸTtc\UQ_?LWEF N=3:,Wu 6sΟ\q?g9 SOU2hKHY"Dڕ%i5ukwb59d^W1nO&p2W1 :Ag nK~=vt-_Bd)"TcA.\YBH ] P){`kyٚ^~њ\)׷ ?Zͤ|coTɂaPG.Nb/ۃn rpRkg+3Hӗ1R໢tpڱZpʮQuP$H$D"#b@7̧C`SU1$3+P**H;Rs"U2B0 jklnjeޑ(mTٹ,~QigV<r6 g?kŇŅ:4AJ0U+C ?9 zb@B*2Q4th6a8$MucV5x)Mn?}𑢘" cjF}Sl[_t hO}1u <?p^ su٢~?> Ơ+-/hIb, TL^޼w!UH<(^itMES{ G ggw89j $',-g%5̴;hs8zpm'/aPBB\!p.7mE+K #>][*;f3P@Oy $ .jws>+A%g=,9W7L#6 `I$LIhYtlْlKOOeT~-JDJ$ H@$",b]`sAyuNK9uRwCEU"zmCdy4r( D?ܪ E #+SO P"D"H$un9%C1(%mg?87I+ ˛/С DRz %v^e0f rJ/Qt 8F]jswYv3}P?g>V0-XNuI_+F6V@_ulJSNRxwQkġGgZ\gPNuxNdS#tREc4o7hҽ 8B FF 4ߺm(bBC}g:_呹6qFkO (hӱm3k3KԄ3uD p^Hp D"cWeAm[C |{i􁳰VmlDw B\n/>0 ` i X)UA FMII$H$D"ADO!pVpfSf96KBpC:'A䥘c5z@2k )KG5_U3o҇ !_H?<0 O*LfJ_gk|McPލ℔@K3k#1]b>Ƌ{dssA*"PB`!tf6n]q pbΝ'(]] .UW;&g @*B\ ƨ=G8/wZ0 hD<*BL_YY?&YXAcF$AYE7dS' DEgXe CDw$1H.ui8:k_ŭˆ|SR=U۵=,rm 7n5M$GF=9c->~|&#lhf vȔ$rC@¥ jM0*ϝBZ"mݐ]2ܸ&끹 Skm3(#1"7LT?q/z~j?D" @D"H$<fX| KJ @̈˶%sQY+ ;QKBK8PW֖b͂6&due#xT^Ɩ҇u]QqRǁoa ෝ: NH1`ğ6۸dKO?r7[0ÃPsCwmlҗ }߶ tˬF,K!Ub0 # ((p|ffb5.|WwRX7\(9?,܌}ిϷUY' ~mp<^S"#[ 琙yqK#'go'XytXa ?EIȄ ]Z:Ga϶0u߹"c5~Ɏw̠lͷv4Z3ɢHh=wks]>o= e\y.; &^B5p9i3Š!ݷE7K "4§ozan8?GZ^,339?][B.wI֬~>ykm-; 锔>B9 ɏ6_^ R|]/Bi|RZTb(~|1+ȦWĄ@,UDV52X h6ħUXK̑#ݿx٦* M1  U(Ok^x.jO#5SyX([#΃/ݓ?h] @5 xv˻cs0?r`fUz>w`m;fAxx!?w!R ļf3U_-΢wbX HLkӱ# ݳЅrDdqsw"OLTW$$%D"2L:U z]*j١0sd&Y$h# Ɇgc^؈>R3Բ[^x!iHϥ &^t[C #|!oЫJX\~ Osys\})^usGqG&)xWa֢B%tg {)ş'{(O #C%ߦPl({1-kM(2߮o[%H y7~곰6_x ڞ<Ŗ?r 91UYfyFJUڶ䓫.>n+dF[he;2ݠBk,#^ԩ]6A tvI 39UgQ{Foށ\fIcq9/<4ѕ.qarS}^4Mw׶}l͌休kHYQP'>Cťc-yZ*\8y&dˀo?tv cyx: #oy:o)w ږ BD獆ڥMbP"D"H$uL"_[;ӱ8 AD\$#%pN@i@L1"= `20GNXDBHOB g}9UL$P'\$D<O`d@'  7] ՟5 NOR*)Npx>4w;L4'fI E&j7W!=rNҥ9?Iqgѕ%^ <+:ĸz+/h. F9>P5FE\}Db4'bㄹWk9wj!(?d*;g.%\H&@F:ďkc؉w^ʉ߱uz9BZe'gW@2#Lܺ1R#DE U R!#OX![ga5ґ?+i H$(H$DfU d/r9ğ JJ\GY=RUU Y%yݘ\ Ԑٵ gG5^Oџ[NL$i ~P֧;'bR`uD-0^@R!(P zI3Ƥٵc̯2U4(>'ڧ߿[LJKU+_4Fv\1|ǎu4޴k 9  gJgv_5 %R A*ZxtgfL%@cIV_?‘ՒmnrtLv*xȳQq|9m>4zML70ڈ\:5 J]>@/LB˧{b C1z,n1GAU"Q$%D"H%7Q2@jj;ںyre9ځ UFNy;R&;^Gk+J](T 1:;vfnR%C5WnJW_9Wgk/L$r :wП B!Х5b";r9q̠ߤ5E3m7v BZ_sfmhE-B-'w5މٺ,&_by a?&\%r wrvN\)PB,6&Wk)RAۊZZ9}@\U72rW:2oLf,x%N&XMeZZ>Li/47ld; #+ݻTD;vSzrATzO._1y^ =xz_Wif=xݠ~e \"Eu6B"H$sUɥ-rH٭k٩Qvrv5;`WeLq|хa?:Y;nV0{!;ev΍8WrG{S#uDZI/D5C]>{@A?~C7}{\r ,^ u/G<P{Хy.B^TǂjuƾbLDGŪ'W|n]q:EcnOP'yn ԗk*ꚈJzSup+1 \lb" b&&]FjzGWy]To iGeJA3O/GGcollN EQösla44[Y+Ǟ}_o'ϓrBkk,/|y-Y,Y`Mds 53+iG)L}xӾ ?/[87XZDEa ীK+c nR1J^z盟 gGM]ᆲeT ľ&-XA+ O܍=u GПBC3N`w 櫑 M$##$WȌ_ &ު)o+xnުu7=w'}&ve[c΅zf*y:E;53ݤ MMpS-:wδ}O=#ԯBrz()ElߑkP642ްm M'[< cXK/ PZ"6^]e(k <5>@[-'j1wlz3ȃsv Wo^7 ,w{ҋ*.zʍSc[\}lEėE^IW%IJ$D"X1T3J;HiG{NJr PQp 1IH PUgi>D()RyU#:98K #Eޕ>EiOO9Y ٴׄ۝8πgfa>obċ,℅[/N ;v3NjD,aPUn}/ :v$/|[ѵ^”! Xo.'KX_CG-14a?Xk5GhƇUkTm>:ZO| D| 0Q=|Ymbm,k\~Sc+EW3ՠ_yN>0GO:Ek G; +[N1t kԾ} 2VuUЈA _mZp U8j7gی FnbN-6f\9qL6m.>ߢixzRnKƐODݿsj zhJB  OT iЅz5' ΒH$(H$D\Щ#(;bA5cIikfvS,geŹ$|K,/tvV衂TFjq,;j'.d(պy! x y`|ORZDkN/]O@% \z llя4̯n8'h_!D "@^_&qj%oc#q~V>i:_0,D%,\LVB6r@U?!_\BT)Q[Dx͋TTHTXLѪeuЭkC3sv0l;DŽ.˕#Y''vGз=ӝkϬZ@v0ͷv$ŋ˟@|zz {)Z*\0ytKи3tѱ~?{qų裋жh56/ZDD" @D"H$Ⱦt՞2_~"w* Fbh 쥵,,%g/xZ+`h,eZ1G.Yj0tԘUjfzye<'B1IGon/SϷ_~ Bѣ*@np"Cs%!˫5Ͽ}Zzo6 dwfFSP%]:MȧRXTDC(x$i4Ȯz׿yk굪%zyDR n!Qͯ}E82΃>%J (|W5$- h^z >Lp rZ@tVtOO=N IWg^~>X,&M@xk)eb[GÐl-Rs'(n>#~h O XV{ 5#~k8Ks̩ _U2'ѣ+T̑&ip]Eo?GV1.G^65/j v_CYs~1!OAC MQ.xI":V!H$ĹP7Ԛ̬@aǠH[s#LqQ M8K$H`F`d2K3ƀ "eA9|vx-#"i\SW,~Ǟ鮱ǁCi_px4^"M4_ۗ9guld58_ՠoVEړ\w)&:zVnw 2j*Ʒw:0FU}~9`Y|.c׋~l"DA%в"+@~#1ZaI ky9  R#:h\ 2K6k}U}hFXIP"ۀi<'O8c6hnη67.F @R<4Gqp\0[z⏏DO*E~'|.hU gAp*B͍74<湕$ށJ|hʡ"fBT㍶1TX[A\PQЧ\J{u[)\ _@QBc"D"H$u?wZYh_Pj͜AQk&{Ggah k}"k@ TZݗ 󳰼"`2943m0 EE͒֟^CwKϻ9 @Źq ?'\ Z7WeC7,L*7hCGWȷlՐgPuuLtKʓRl1`:A  qz>ٕ/C'(e@~#n @ML4(js1&Ȃˏ3*>xz'UD Yu=@|S@q!h}eA]H ƟE:qiw6wt75GУ\R| āQ+&aaygxѸ~"\"xdNݳmJ[!$.I8eQȰt-zhPT`CF Bv+Z?w@**b2$%\"H$'Y,GUJDT JfNM7k7o ,q(`䫟'_/3j9so3,0_gbi 7]O!#f^"b}Wf5sy;WZ~_Hp1$$2<\}p UrY-ynz BA~;J>y"2 Z8a:1švcn\'ƕ zCB!u7?!$.wV&ǯNjrqP)\~qyη׈ K:5{H!&''.K| t~k{ x:kWZ<4m Ԩn 7lC&mhH/GA.=;1G'жyt*@ |O-us+m[޲uk&ԍJ}$;=GhjX9٢4JA.EnXiw̠O.AK}J?שOdH$D">ڟ>Sr Kl2YLA ("k{:v|כpU*Y c06 &EG?_qAp+[eU"jqm}]??PyᖠXZ#"+(ȖҶK*D˥Snߺylv7 ZxP%L;~y`QNuķ4M? APA,vݜUE #2bEXAN`⊇yfp3 n.>YuV@n~9d^I"3,8)){AA3"UWt8T!˂}C>o뉿^]߆?ڬ=brd0mslܡ:HЦ/˹;0G#W?1ܺȌ'(C;%=\;]m{Ϡ3`.wǒՎH Z) a_hGw^i-* 90 [z_Ȑ}`b] .>}mUx$Y%IJ$D"_]"t!V!4h F՚v{4Lb`| FPF\XBCUy%JX6k`<0Zq-_oCc> |?Ko.G߷(:gZ$bJS1=/7zI C^_:pqK-/"@P˂ԋ"lq 6LdUb؊G;)=hQ ݛ!Ek?kA?.?cG)]]и(/ }=&,s כh|9d%~~1*Zӿm~5#D7i#z/\ |70qW˗W1&ags3[SUw,}O.VxCdWO!#5ptڥuV=L^b"K8.'rh(dl#U/G7 ܹ.Q>E왓p'_(ט@rhf=E^GpZLQ aq47'x!Ώ[ \?Y+>Uݹ_|;Dxqm kӍ.6 مcdN@yӄFV.B2 Tb6zԸdƉ:PFO(VWuͶz"N@HǢO-hpa$"\bP (HP"H$D? V1Hjxȏ)7jPQkPXc5Pt8[ny!(S08_)-bZ,hPP_on*Ϊ_,qfeM7x.lY|܀){n(o-K\F:?yԟ14;N)uEA[_xSNiҟ?X~% @w?`jy cP#TТ)D&o}5X:JY7 ܛ]Y8TZQ3$:̖}C(L){J[+tcp={u>ʳ tP"Ϛp3[ )vV_5aM#VAOD5C[{B#mKVJEL4dWLQ5C 'W M(~ԍphC9O,uyrP@Tx(.D9ĵhD F F`נ+ X좏/|'}F;RNr7k$pDD"H$@g @6}Ԟ?4#Gj8Oe֠6C5C̎6I^,舡<ݔ븅SVI,`j;򇝚9!㥈:LQ܆U.T%=gu9"c) {y[dKrtm 1jx39PAcDWA(;'gʆ-Ɵ:,HD :i2c .bM%M`jE2㯇K0Dm0; @ښx\qrXb+rݗj9xkYʧ|KPtqu6NDĘC`r5 6_XߺKMPڬJۺyt Xn$&xL l1)l#~^!>}{NlP|fl6@6%*Fzp?VG^D׾D$Xppsb%(8/[HxN90D9> k%g|8 ߿pǡ{EH@DD"H$֥^{C,vjED˫?U^6zv+cq:{9y:JcG[^oV܍9t, UW A*ހiUb.ߔ}DaJ'i&g iP"3%{hiYEѕi5;n^o>*#]$'õ}'"8ķ0ok]sn炾ߞ^D"n܁17E5VL%n"TKP}4Lcd&Sf~̅!C#Uki|.ty>>B%MND[,L7зs/_%C  1QݾW%apMJ(~EQ>G@#J9pјiDtz~vui.4/(j1gԶJ0.9ZvH}Wswg^3UGp!VU QIZɗ@7x R"yZD"H$猑 keͻxAFhFEPu7VqP3_bYg"X$EfAD'9KauI0F&,RTi{:rz:+h- =Jv 1 @m%['J28C ETvm)?'sB~&rquhB*O]CH֬CXP-VCiM݈EU}zxFF܈c:,P{(;<ª/ǭx=ZR**]=#D|YoZ5~DkYFcq,* u{ x{ӆ=,pM@muϜz-6Cѥy 7 /e ц[`{*  ^n fv"ّ6ZP׭Xwl[)P읻@~_6ɁNg_aO?K')WEE8s$j$V},Z0穊ڑ.||'gDEc\Y OؗEH{{}X>`d\<:Xku>[LT#e64"G}WY]WEZ8Z3kv,>n%KT̉" Usgz9Jf\2\=9O8cAu b݀.H$DbpO+vŷmݎPgy ia3rd3zPUdNy v:i+X ֻCF'`domn/VBv am  湵 P-[mc̝9h5&*ʒkq]f5;v=oGw{87> {&:ҼW`džXר^8=Ҙ1sK-(N|r1!1U wyA, ]Lݏ͸dQOP>y/Yݝ;sq_O|[},0qf|C+ۼzre0molMݹdL>[Co3{8f3 \P%.PdskJ{\ V,@ }E*%' 4݈DDժ$YY]Cr(L!d}`>v#֡{7C\HCo\%\"H$Dr銒ҵu p(vgU3G[,#+ @MÜ-0/b7(/@.c-ހIϋF&hfn,2yK ѪzUA}sEO>7DQL|o!K_^ǹ& ޵!E/ZM:CzI(r 3wS BA.筀P5ӻGar#w`g}u3sq"~j+bQxYRڟ2p;cPsi=ۧ^vpT$*wF0{ 4\k׉G$8s ʀG^  Ϻ ɌӃh}>;hq֓?z%$҆@\sŸ5*0a̢ W\q|^U0B&\"H$h-Y_X @7C=A:߹wۦr|Ủ-=݆2 KEz^Tt`,?.A%3Ȍ_{200Ȳ߿B%mH=t0{$U#J(Y$,Y͒yfT3QIN6S췀3B-s\ğD"bbxwcs *`PI>vd:x"yKq6EUG͡. 3C_Ud`ڥovqxa2݇?Cq{(n Y5"xP_( D#(K@2X9`mlLޏOaNJ*ɬb-# $a#H$ׅa:nHYg.W'8wMǻi&`#uS :R4M".V5J E!jk+$0F>ZB(\c'8W7w&pDJ$D"Xfp"@(vzN&ؽtv=Jvp Y.!O=œR\:e`PmV`j34sQhBk:L]EDL6fPBCBM`i,pwer $"% w{7S~)H>ċ@AݘwwfvRLc3(`{ȶNQv1vXX:QP: kv_bçxgSBnhZĀyT\RU,\?HŎ׫~F01DbED ~o#G3Pqe]\,̫c5%T$%IJ$D"Xo|^kfY:vr܎򺉧V0Ggmc 1P;L"=RA_rg-t1t0G5KZ)KbO{9 )c@B?逜`Nm:XՕ{)/E>vs3QqpdF;fnVAP,T%noBgn->~O$_5V-re=.uj c娥"ۀXx#oE &"(N4w󖻡Af(;".xKu}[,bxk "8L|^^ LF"\yENp{\4&'$n-{_$"x' MZx4WB #4>صqK^PBxu <<\~0$ "))˩vX4iӵMB4b檺L":#P վC圭"},zM:X,TxzRD" @D"H$QY==(vfb 1O,#eM*`Kj97Hq(ZTb(PX 08KB{֜iA,T|9`f;޺G ?ŗz"]q8:U@V% R*K80ϴ6LrP0XБ;&~FqV<5 jDD⫎zq9^Hr4ilQ9-G ]|OY0F8oWcHB=lZR>y t 긼Pʋ+~ o|=Humfsf*s B.T଺TZo\a7\4j%oq%&hX @l y~T"N[I.\B\(2yDy8|̮v)W-݂FUK~9[ N BrT- >^38F@K*.ʫjU$%IJ$D"n:(3K]:Z(y2w96 Y)5;G( ?9!ŝsgoӽn -(Lnߒ`u VVwD {ș.f@t0](yC@.8Xta5!lZ,MMTD XS|gɌ~?x߁~8~$ "e!h ӟ\yPpEO$U1FZ]WFvBʒu7D &EL nzh%N O&\Ǿo?W&n?zju1{վc<11e#!&Xu₊;1ci8.2>"cVZ |x'`b׈՗ڪYjqAX#CT୊O"qTE ]8z8Q'?Q7S_\%IJ$D"?e1.<̍ ynQ+x[):h4 .tT ܰQb*EZRYWa2ĂUts`hGVZ >(0501+6`Jnb4 ZzOy0Q>/Nf͋QEz Z/TALnSA@1vMIğ_ܸ1S'H|MF~b[K'hF1w =,ODŵ#~wZןk7 pf+a0x `%;wqPa 2OUTQK}< TRTH*yo|n.Bg\IHeƆ^_G$u?Fqvb(fy;eہ>s1@sv-&Z1gWQ'$."T(q\[q6ގo5TxO8c$l/X&EhJH.HP"H$Ĺ;Fu̵.Ԡڕv%hER$!(#i,O߁Hp~,G5o3P {vdZSҵuXg=Kl+Q(+ s N.IpDË=E4x42x)":V!H$ĹP^`}п* eG:75|Bì./ie067xb[ vԉ fr_U\(/t2jAq:.5֠u~(`uZ޵[U)7̩ѵ%"D{!ɷ]y}-"dEEYC\dj#r]B pbSQF'3fcG, W)1ggy$꘡8O}15]h4S:sCd-#0<* h]"D"H$)ۯ@1*`gUJ,N{bA+T:%'/ե͈!3YoW0: v) >4!oC}i[janup 0s###99 Naᣆxiy @gZ쩮ЊU̅mr TMTb}}siMSQĔ 8Ͽ_Пl /NȹlI$/9#As=LF>;[HC  $=\f#>ğJ.?DySPF/.8T\X=rÌQQHL6~\j-"Ft^M& aGON4vGN)ct1Lq'D s+u1BHl ekcay UynڻJ]7}<חX ş׻-fuOq혹7 29qS_K\7w(x&O38sՋ3)ӝFwEXfYyZeƁ@L5|#ôi?紋(]8h[O|vqmΰ?VzZfw@5??!oևKR"M\gzMnN"sy?])9BP= MP"HP"H$CbPfN&*dlVv4aE>B7J85~QŻBw#DǙ(y;߇w4'vxG[0lHx f2 5 _' h$nD@` y_Gsдt4z]:ZZD_@i_ZyB)`vd.?Pqc6 RtK.CjXhZr V(D(G=\x1D@IJ$IJ$D"| @ì'~@.$8Z3} bFu[{MjkFϹc]{An1+c̣dW7<͐@"fJ `%a$T橎/`Qp 8GҀTmaVqj{tQ*`F<^?dva @^Y'/D"$!r?5:zYCƷG}6A~,SNq0ZmPڣWŠ_T<a )b*5jLJ)vBPV/*5lz"זAqw]hD:En.|IA-G˽.į?؟~x`Sds4/J F((qHCQZDQ @.?J$IJ$D"| @ao5'#]jT3) pd [E @/su5 ckDhEcO?6,cjVn7/`@ur Ԃ ?`0& ,Pk &+Tبk=☙ْnzQ /sy{i{H$(Zӂ"A::F$ #~ $|-5y>믿!eP ΍_u*}z\ڊvs - p1Eun$ٵiߦ=Pti9ǁȔqԟ2lOfe{cnZ_û\KFU¥0c@LT?-5*'2XU-|m-m03?y /|ρ;$PH$ (HkƇBCsD3bsې| @(ُaH5Gc_c\&?^;UPcczq*7W{φ8tygm۱$k;fL0Ná}.zUH"ǷuϵcqbUK )"Otm>T6&}AB&>e"cąMLHjʻ\Iѱ/䅟 1v3OQH$i  s%.i8[9MSv?;bTVR/hT$ A<"Es@bJGssF_շ_ v3lCo&~}P=d\ XҞ-HDލh(H$(H$ $nu?8:FYIA9R{a oGPCg3@!X\ŽP0Ԣ+/ abyA{x~9h.s< q% /naN!yB#(TYb?-[ۧn[<,U<𷁿thg YaH$4!~/8$p6AX5KC@@E [ `- Z"ŤY ^h:wX)tzTT\;uVñ"k5f`_;%GLBv,b؁oj .E=ӣ?N[!,rNK>sòuouzs/~(o96iUQ3[9Q5Ih3]Q'2}.,QB9\v')}KP"HP"H$HOLp_-8vEq^[enh62Qu]W+ȗEn" V6Q_}L;WMkۃc<DǏ+ZB6^}ބj1G)Y'-',\*v!fgJ ğU1W;㗀I D⛊ @F"HA_!GCb- CX+HyM 8uDbD)qc5v,^BQ ]Ii> WEPUg|۝m"KL`gNpvNLi &2o|?Fdr0$rnln#AIK4ŵ /'C($tw1(m@{>@D" @D"H$^ (ͩ՛VQdܣup b&{| ƿ(_5Kus `P'Π=D+|E-a_wm2œAY|Ny"~\emla:STbJgΕ8%N [,W+D"$3qJ̻el&E;| Pm~EW"bR1PJ5:r`rԻwq p_9C{G?=7?}0$XSŷ a}yzH0V;q%>Hܕ_S((;l)׎ƎM/ZCU6L.-4qQisĝKaATv/H$H$Dk/rF9K9Kkn85G.&>LС =,@WnLzվ.lQoD 8ddO0O҆IiP:KyЪb̥ȉ 䝉Jn+zhD @qvo H$ ch ȕN{ t1n zw(pd q)C_O4#. LۣQ_O)E~;>:Z -D:TbbL':N\߲P6R%{dMtwQw@wX rX>ƤWoJZ=6*~?t|v_+A#tzrn< L$딘d1U3wx~Om336H"x@^B㛟 'R,P칟l. 9X yϐru7D<9Fb #p .0\xLe Ԉyx@cl52KV_eqf\>cq!%wNP"th8iLQ~۝^lSӚۥ"m !&iqDA'Rk(HiEڵ0$sձvŦ]:Bx׵IcDZ pl?B'>]xR k}@͟D"D"H$/TY)~71ދZ ո)Ǔ׿p7;8t */m= 6 sah hGToBKU`#Ǡ1  c.Nk>ծ7/\?_=tGW<{LdY酨wBu5gbP O$T(AB'@&^lq-߉{pbj:y]3LV>иBQ% Mύ9$L"BN0yҾ_rvkƴ!..>S#&(cciL'WPjG[Cյ6rFumi xLYO8bJ{{`.h=bp씹EӞw6XbcS-αY&jR 'b53an"#A[3珟UyҾ @pD" @D"H$oȅ~K9UkpPuݻ5lmlW_H;%z"xEc#(BGC, Cr"ϡ2,g@ˈ&}kmuSƏ?6mP Aٶȥ{L %SNЯFS޹.qa}ZF'tW0p+Or!YX/ "FQ/7nag|)O$ zCSRqP6O_I} 6@<>4[a#"A5_N vc$3:(tC cAgzwpumsx-'`ǎ!k}nIejQ#c= P[dBUUp>7czx_WRUV9qXm3) ݢzݢd<)5Tϭ rNqJEjq<٨˨=uA@@ucMfT}EZ=Q\B%P"HP"H$}ܡsoA(AUh9`s/A`pF#j^o=w-l!o(_@u,|> r)uJ u6Dx1zg<n >1sqRˠWeh?cg`V.U9VRt^|czCz3#CS`.YΞ9<ᄟJoD"#|#9myɝ0;whE|0SFrΛZaPe*fZŒIZO-Sh0JF cH=f[/rܘxI^H(scT0gm\_Yc1Nb]ܵۅ$;%?S|n=\O_l!V 9r!@oc᫕D"D"H$/3U)WOl9b48fvvMg 0,^ K+dD~WPłɽKp^A|[0خ˺.1G@ 5"H1GSëc"IUq egbndB5\"`2W 7Ȳ/zYVNѯşrLQTkZ-AƂA#QwlAm։D"A;g"h6dr#T}бG;nhh6^$PTm^̦௔[~n1<3jq𴴵roLJkEPtJ'x(aJ!sMc\ % F89t:U^d7E8 n8'Nts9&?ZѤ^n Fe/8KZ9[Z NR,tąQ8xby^bNn̅Q$HKx8 `wb`vބPekW>SgffDwfB?!ϫ6s͆re:ظG\^%.{.s"H@&ܜ%S,)`i ~C8Ep0RSQ"1!.65[G`)ud4A>-!v"Roq81D9F &h˲ u{U8AkAPuaN1ޭuu!JDs̈́DzeŹa@D͸$'2AQT8A)pcݹBxlfvLՂP;G4B?ĝC¸˸]PPiT"Ny?eS ׹r_9MofDvD773)ќ#W.lTv#O_HoD"h#i8j" F|^WXEE\_A{/2ʨ /qٗvRU\ti'C5="-D"D"H$/O:o=dbPAXna&r1:u9s=xc3Ng,\}"S#3` tͥ!8" }@x|+"G|uNj';1Ưࡘ_/j?Q >C}cv-t `+gg̎Ό&r*~yD\>x‰D" (-yv5AQD!#a^4 sBN\_?sFǏCIM]ss޾wߧZ5-MKqxLJqOQyyYZKc$ST8>ƈm esnE?k7P5@6_,Nt~/I0P}όQ2T9 F,< r) ]=Jn]@ȝ+q\Eq9#VsPr)D܆H$^|tD"H^ۧl!"Ju}+Xqʘ avmo^-; pow#WaŜ|_5·/{;\&pD¾!kYܘ93\20셆~.zM'`:< zy*_,+7; ?Vs~m!! g/o πAz&pnv?񇥡P]4)i&O,τn>\L"*=N~mlo Hy;X_o4sk`s"#0bB|nc)A2S 7NNY'T(y:n=iݟO~ieAZD % :p1 No{;s\<\;^ 򯍻>ן0?[9gP\ >Q 氊)b2UyB0LAp"M!x"aB@D%G_Lw!H$Dt*>@RY]M0e3gc؝=X@M!X\%s!=2C.DxXK4]?Fz߈ZFwv,o1'ߙU;s@F>`j2uDbwN .MAB0j Zj"!4.B1#M!ߘ|~є>-ހDZೲekn2( EF)2#˼VPAfFJgWˊD]4&]d]"[\YI_1Si=:: 0'xS>,fFa1 @PYKs7gd퓟G(SD"93sPff!ҵ9׏6!B0Ձ "~na9\!!tc?2 #ףs=4.ێ|="3QBITh k."*Ap`v*:2/:UZ]1qg&DGvx/mqPEkKWY`( F^-Y V3TnDC'45K.bEL{1 [d}B*#kȅ(Pd}czAcc̀\?`2Lh2.Ǿx2ҍ#$_52ucxV:H$v:rYfQEM^$ þE%5ub2~݄ ٗs~Y.bo9:}['Ȏ PTe}+jU U}F},3[Y Mtǎ&VjwAnr:eZs_vR ZZV˽vV3 "8WRUB[q`i|]Ӟ;ɣcFUB -l1}!q/أTDx)K|0Ƌ>F@2nBʊT! 9bh=KO"F,}<賈( 'az'Į@q' -͋&ai! 6s<^h2>_P n!IUAkRꪄCcgNKY}xnٞK@[h=e~ʴ0f1(kwVstQDu+rei~Ğ^<2>i &AڝAJ!B'}eouzaFU |jP.3Se섃CQdWy|Pd|7 S4#a(vEfW3(Lq$ND (HH$D`?m΂x-,A5m#Eʉci8N xWbU A/Q A&k $ Z2W78v #dv+jy{tUXs/4?@xZ9$k@v~Q/k's`&#'T5xb iJoD"؅$I4(2{D*6C-g jdE 31+H!usd_>]X(tQ? i~$& DE;/sJa`UpT`(E-*g+-i v7:L9uEPth͜+֝3TVQh^k_^sx6EW+Q? *!KPvl.B@ ytq$%.H$K,"v/"?/ya~L.AJuKhqr"8Ѡ`ectwpE mm [X *Ҩg.P]YXP]?a~ KO9Ka4Bώ_zdBί BFڻ:GmnFEgKk x(Clް[\)qэ` .>RRO?r 'fZWUN H$ZX86$FǗ:J4ߗФj,QnF;\R{K ٣ y]~|ﳯTDHRR ?'ϲʼn;SamPeb>~Gd&*)D h~uJ 5p#qF*5BpI`_cBE?ם zh|ԃK-c0Y.8fI[о!KsfZ7ytN")s ;` iGn)_H\\X5ɌE` ֐e%UJrxmHfb!ksy&?DvlNe D sv&P"H!D"HlXFX@QD j+ (^)EzͻQP#L&QV@Nhtus-Mw%@ ۽qY_79;3~>`}@EW'0hs'N WѵVM=|noB[=e_ߒD Bɴ+klGN!^DC5mYYq>4EZڸ]W`ϖ|24ge9Ñs[ȋ g*`ZQgaA0&_ `m`+/>|G71@|v}̈̄S2΋Ƿމ/p򾅇Xӏjk䁑 _Z' SGH$% thfUTBEнc|H0%x$"I[<  vW?#ûv[^wx)I?T(rSQ Ap U広[d&"LUOoL$^w;;-s1h`m;9Oge`UpCZ6CDr`ylR`Z^h=8_?]/pąSBP_Wͨl" (|ADH$DE%=rBM_hbfr; X@#[w,qFUÜѠp`gkuna18ώQn yg߬m A[H mFZ"wQ#hSyqKⳬ]z&2x㡈`&oX^۽~44gR4T/M8D °k^@hiay‡ \ϯ M 5k`QY>{Oi?HnV~/[0) _9g2Y3?9s!ܟv#"ivln1-̢Dݺ--cW4ַ2:|(8g`) -GָfߠemZ-^:%skȪA5k $(H$(H$ aPxiXk<ѸO PEƾc! O2yo鬎 Ih``{`M73x+bkS;a88\ YXE'UL`4mc@xx7"<_D^8akM΄ϰzvtgzF&\va+A#K4I4D[@ A&*WU&P0!ܸ95ZNN"d9OÕF 3NEUQ'[puB0zo[^ǀ/ZyZzf5CD]N$%$%D"xaYDNT/[hVU~ZGoyUm((-͝gsv ;h{Ǒc7;) CJ}k# ֕ S AY Ğ"nP#~_vwR)z,ۤw&^:gGtcVCn~N޴zMw:S/rU a94}$WtP񮕈&H(8=+ڜ5abR qD~x{W9qq9Ѓ?f.oUl7W8ŪZ1([#t; /)ݡ3D۝3ߞgykv)׎?!23tV mu⪅U\+]kZ?j~!8Y'24B $%-OP"H$$tQnE'BN ) xN ܞfnsv|+ ݓ) 0suq[ȋױ}|h<,(qWWXXeoN0ؠ.ǣhӬ. OCp'qR(:C3@ͅ[~rQP;Oo[$8 9 #Nd@+&H$HN8n.~!`JX8G0q}`m³7ċ_9aOL *d5Kcs!uM!wl@DiZB-BLYwƴiuv:vg [w5{rBG]fRfoNfP^"3*O0Տ?ו@DeMʊH$7>muh ;P"Ue,vqE R^'A("vTİM̕&[sW}`}NoM+\ēplAUBSoyyެsŕ{(ZM*K;ףOWz&{z-*9qjʻ5B&]P Nd=-(ѓH$^4?20CT%J <Q\~" ׶ğ ޛ3]7+-1`RRUv8dR;n›lߺ+?⏉D|Agc~0uǮQD1m'l 8#my'-KIuFs\ޢVMU82'1{‹6wYo7p?aA$ H$(H$ rm ȡFc@ "\{]4FiGD*Pj EǍ*ne ̀1'w[ڤJN= +a<18[9+p|6@C^@V@߃ץO<Ҍw7v AëV~]@Tz&{"C6>b|I55 ?"~h eTzEQ<}%W, 㝷 B64x%S$ަ H \pB 2"pPC^6!2CrgS_W P=gZPK]q`ion!/$⏉It 5vA&?:c" Cu+m c.XN5{q[(ܺ}ߝިA&eNU6ރslKȼu"/>'?q}q7HP"HP"H$y GdTPx!D\ W(z]~U/ (=3K.s܏dK;b4;s{ovyI(8)es慔s'yXԂK SOg`2L`'tx, {5ߍU n5ן[u. A }9vE/b ^‹&ƉB"Ə,~ /"`?]@5}O$`(MK#N"_^Tr}>+#>op <:EPgۯMu_z=`lAeJ <ﶳt h‑HmZC,=-ab4q$v}9-NZ8z}mڅ,O\`#qΣ,r;~h_w*묬2*+cxEwn6[\ls.rC&o/D"B [QGĀjnj\ 󞸇  z$ZGU])_=pJ  A=RQOh˽Cz!G):(ln]!ڮP,Ru ލM.8ėa?N׆1 QOnfRnaJ=T,|T=@ǔ~Ѐ}JԺzߋ:Kſ 8 zT"x>@ %zEx1G"ljTo'bD,R#(/f #VNyE]jZx͗)r;%Ọ:EaS R9V<6lǚ;p-\h_\qyc?NuJX^w=ȢΙ޷bkg*#b4G=>oEK^z*o@/'H$(H$ R ^zԻy}//C . Fpnj&#](>1C> mdyDZ0ۛ,aDORQ߭֋AOA]W[|}<9peͶ,u[PN˪\"s!B@X.c?.0u&t2Fʟ^[̵Vq"":kW$NvUbWRvğ%؎X*%RDmI7Xf`[nvu`P]}ooos#sT]ۻwra~/-2>wNj<"~"XAIJ$ 8B"H$mv'Q JmJjhPEe3džfΡ'm4?9祠րK&+8zv?Ze˳hx+Fs P__x< ≕;DdmDp sD2>Ertګt:'NLd=XdyM#Z^ C@ >[dRA#/L89cܹ?YHD"q-1yŖ@Fy; |>s E&11)^ô=0|X+*b1UP1:)vn $AY&=q 5z6GjC(9el jCQѨf ǯ{ĘC߳V (Yr"Vxr?;"4S\MH$(H$kQ `Bq:B!"-6Eusc=!/٣Dzﭾ fNL1X+jyum]XcnT$k VahwCx?9U1 ˪DQH[[ $jĜ(ZN}꭮|}?n/Ǫ)S&"Ja3oYgy3j=>y!woD" @D"H$^]r( YEb  UeP\YJ="B^ @'|UZǜPP~Sl} [䎷8 \]4"Ď5(ʼJjY12 "xWY'V? ?]g:ʚ `c,b ۲3w=ͼG)qI'xML.<,ZS P%m,oD::_':D5"Z T$r^'{|V.N 2Zp @ MH$(H$ī r@3C9( HU6 O8fܲAOsZEɿF(-6 j ald=Zh.:qغphp,z7ZshBDzxvA"P!rs .HXQs {.7,Yf @6`\|Dջ+ApՋ8Y` ]"x2 W. N/V]" 8Cy?05JO{+NmjE+ &ƨkꭕ±H]EPTN--ǀ>5"P"׸oD" @D"H$^yvc J|\0OhPu[ AN:|J!UdX~CtcIk)mhőš@5HP@۶񉶏[ ՊKHp{"DmyXٍ ;/H#kȥlVWt7vڃ:-r`- p7é'υqdhO̵ TKͦV\tɦG_",oLETň[o;Ñ}q# q#/s5Rl,߮py]"m䒭gb󨷨W{O0]s Z9<n `i7«LoB㣟 x'MH> D"Hz 'Pxu@ZrqD\yޏ[oxp~P'uN7B~'JGA tvn(%! 0-P c Y[K!eGZԣl|k~2OS~cBA*j r{^CE>4[Kn]4nZ#bE!\3ƟzAfCGg$0D⺱xukD _se\tηOrfe1 }cз7M.bjD:F_^:BkzA#~ޚ(>G>_$լFѵE HpTX#t33Pۗ?eYc) AC/*a6"'Y'?LZD"H$^2T  >NO/ B/UErc9Pԩu6'6G4 .WkDx?I0?.5.\]JȜpm, dFG:ϡ=sXfe c1OxOO"D"H$ Q}psxCA5[I%tgm^  ~g"et-j#ޡůWB1EA V4?9U1$K0y/ɕzRm$H&Eچs!rʧ~RYpZZO`}ksa-ԉ9L. lCn"IEz17o%̱! ܱ"8H#=uƎqX;(*٦I`ӓ(@wОƻ Oԙ.FPdĮс1x$HԜK;075bQt⢍ֺˋ֮P!>L ^#x]52O#ވJoL= d `(s2VVSR(sl/D" @D"H$^T[x;#3U hZua¸?21s@Yj`%#3-<;< [颀NT)of*e /4'annY !$WS *%8VZy 27Z3mPu@VD6ӿMh3"ZV\{nra/3+[3"G.ԗ;\OUuBQ%P (=Hfv9k:̘m\ 6}1%IJ$D"qu1r>voQ%1M)+}$CԷ']jw A`K['id=tлG~hvGcCi*`1d X|?x%Jk++&NRK)L>3P!ws@!l0+EU3U/0l8C.":͎,LVssta(P1[X,EAE-ĵW zmEg[ zJD"q.J 5n/鞟1 oO }'΅njDE"!Gc}]ʂ~ő E:1ėE%@e'諑7^FDYK5t/2`~X>\ůg@$SogπP(>+'e$S~x2}%IJ$D"qu~8HCĠj[hod[6Pu bMlo ڋo.S#Twf|rVgw EP9}n?PyQىw/ A9O2'}eiY2S)d-SNS ?K.&S`f ̓=ݦĿ^tCt߽CsQD)XLf9}[@Ejɒe޷9O]ޚRGu5c"i |=f.H. ?xMOr,?a2J$^i D"z{ڀjPTqHqFDb\_3q*(N!.]…TPDE T]vpmaLv[`OM]T7l82*Tq-P Sؒc NmBC׬^x#Uwo!ׯ ?S<\:ol\\9qR`=)Q'Y`ӷY"H@D"H\hs84d*o/!Jo_ :_qm2_UqD̆0襛q4G bbȎ =Mk/ad*=49FX)E~ 23=DVF T]59t:0um]`5tם-oC\] `f.٩6 Q-C;J ff.1$Lfyk!;cߖQ2|@:nAA(s">MU8&i4BoGE`q"r.oKN.NnA0 $Un57D%>bA[vqnr^˟kR'<kF+7a}\_XI1@غZfSc cqIZ ܃P^~ɹboy~QSJ$)P"H$/P}DZU4 ;' >ŋ?HqC%ș2EPRՠ*e~plm("^"q?5Am;-9hV)SP󓳘bҘ NXZSe{t<3SeYYgRRc_&]xhLwW`'`m {^aw}V"d*k c {f%"(RV3c 7cV)JBAzU\Ƹ1ADee1"U .rוfw)GB:n\bNXC11 hȼL,/DD]լD\VGxu ǬɣmO8<0'dxr#FH "r=7a >TzJ$WUH$Dbszmj[Z*BKNIBJ2܍]1U]N/Lj2í&{^736Kpk#8r,RG-=:'пk 2w.Y[7wV-|Vau2/ 7!2u13]̅.2ۭ zo݉=4!3Sh4KofKY^8ȖCC}Oy4Z};C (rqnI:37>'MED0"?7`>.Du V55R)`.=KeO.#]%!ymwn[uo#w~hx9AYH<6GkD!rĮ.nXŦQ %P6ã \2tnG + ,>2\"]j1p"$|HFqʼn-h3N榡({ߥ\2?sų34ehG'o(Y^eN goB 8ht;wn%ֻg/S7]([pCeTZ0"yU` XMD"wr^Џ#K:`c^P\D\05QX g,nƤVM]Ո65ck_׏׬<2..c2h}IKMw/m@' >D"J$D&܄ _-jn aP? ]#4EE&LBQ2W*@gɌE~.S Y>ʢ04 PѤSȾD9Po1`4?5CߜPrX]6f YX_+r.>fOF(<-2-).x clC~gAQ E`_ CfvL3V r  KxY"Vbx:|`"sxWpac?A  NGw"ց!6G v> :vLPEۀR # &ܱ.l_=&V4FNLz(w6csհmxDDbsV@ӿgՃncx|3p{lr`86J+b@AN>֞+yKf}[DuB5'KDplb_+\&?:-@2cɵm"H\r4Y&D"a@PkCKU^ԱNQ#PĕCŤA@me;O4qcy9vK`Ar3CbTa?6U-z /}3=z`}E{0d|[P[P=04fvA43+P!#؛G! ]̴^Ā1y H&,wܺQ~l] WnuU̙vJ+_ZO^l:aoqXEӓ`Kg,M2hi yÒewe3$0(L.GfgË58Un^b/:јZQ jՅ@@;=eZMXG~ժ@'E tt?z?jXqb]ú<i1j-q&N8'jE|z'~N/2N(Sg-U(~&$x+u 覧J"HdiD"d?pt$@5n\wDWA#BNM @UY* 0L% Vf.ytAMXk PTk jw39kM:aW[8Vqf-`*4kӱw 4>wY-x#أ(Y y$?&4΂Σ@D*s}*f9ݦLDx6'fevG 0Ǝ@lZUo™fiC thh J(oxHqΞdq}\[' ⾱OoxEU[Q{uBUpD| bPŚY8@?=e A? <-tZ .$QP O@(uk"‘byMXuʀ>xtl4P||D;)EF}ԖyиM_(jp,)Tz$KCPD"g 7;+?T? T?QMn|̏n1]?~XU 7TMSЀf]GxP29'WK wFj>:cps_] "wp}]`w4ѝMx tT DqwZij 5⏸,,jYfMV>kr?ШHPA'{\L 'Amj$g?#h/JND +c;܊<39lo?J("pBъ7qe@րP3٨a|ŗLaA'UyƊ\$^zۀD ˎhl-}M\]&6Nj,"Q>kXA:1tfGd <x~8R.H$ D"H$^ -7&k,7??CU}Y3_AsV#;o٣'v?mxx 2:MVW讵8r?a(},d ؾFn-3dC՟󪁗+ 7;Sm(n T}] <.s[>\3݁/\L;[a0uW^|uiWrX~ҥ,7dz |{ig|al'% ;ޏj`҈Ul1Xk\U{ w/)`9}$K?R T D P6IS⬀LL@>PD:adA7s6kaTg&"6a=ꭝBd@ $ dBTlu ^t+ʻL 4,D"$Du z@ǣ^+}_&+UG\-D9aDc[?G7d(J>{,Ńrl#R`Mܱue߹iX0GGrz?];ɿ|{`KY.V>Y#ã[-A]eЖw MA[jk}d@溘I֭ķ,5Cg{-s{WsuUwWϭj K HB#(q0!q֊@&`c H`ZHh@-ԭ{:u:*uM:Zιgy;(p頴ߙ$HF!)-@ F@^l[<TsHrs!b^s,'2ƫ4F$ˈawougCJ@kEǣY& Q@cBqyx xѐɮR .!SW M!w܆% & c߾@2+̕iv$\!pS&} Ԡ _@P-<)#F ;e7c"@&Ox!vZ)z@g̎,9180l-3 3RA@A@%+XSIJ7jI;af`z-֨>Ex44q(g"> ȩ)H{}_HP"*4J,7l ?>e؊{-ls+q,L!N &geB`O굨aQZۮ74R@T:3Vu$#4F37 )Q%~~ CĔL8a)_ Lzg oC!GLi#zsȯO!}ߣgm7=j$0~X@?_c8#cf`O[ TPPPPP-}91"Aήְ3ԗ3!U;v@ֿmVSu[fJpBk8;iGɌl[<(g_}:HMrl^ZM 6)) HDбC0_@|xݍgxKa+)NC}Y2dtESz1y#-#;|%l#޾jAN3'@ @*k++Do*D+ngP nrxrpAPTԸUk)XcQ0PX%s2ʜml 9\0p*(((oG")^$=}ʍ PmH!SaOAeyJUM{5"E@l Y2Ϣ). )ҷ@ dm7"hT6x%<ӖC褲Kʷ6m]Wyw)h;"H0Sr1ژRISѰ!խE/NꟘ"gĪbIx@斥у0^Mm0}-'Zp*r| f+r{,>,2 Q-WCoHKܵ%.?&p,~B @𤏀`X={:L $G*W3%4o&@PHHu\VH"h1؛?QHw̑ޙ%+H쉴}P X.$- SZG !:`KX,Cxs7ƠCõX{q!5}j Tѓa`<0O/m@pmZ TPPPPP7цx1L'Z!6b`$RO|^G1d:dFE=h簚J+s8=]DM9UL ւ4&1g>Y4)cABԆidDG IHD/5 7*!޹ -N#G֐**rbҖԥ}{ =elinLKܹkK{ #ȥ AtgaMeo)((((PAAAAAA bu̷IQ_)@ys&|;7ޓɫ`}6yo,=h&N{T>dQ eɩG5..ֱLSPPP!%(((((xNCm(oD X4FPMJ'YP1/H*;2TlklP+G,7V'#őXMP (uʺUy*>i:WiiQV>E3B VG 6N BM_[O ru:jEM_t 6GiWY ,c`~bkKK6 $)~%Bkœ߂j{cQ PQ IUR ]X)>f>)R{O@x2EBGIR!6؀ȝ-e-yuH*'=B5'bwlc``Ldȭpɬ0LB'{ \x ԏ-3@=0\\kAߊ sc\*Z=J'5du "XOXϦbҴ NFj !5TEQ"*;m"ϖHLN/  U#)I!NCl+Li'X] BU,M 8K醧}tQfꆐȠj8U@F@ ]Bq H䈒0oHDcPtuLa#RVby04P&Dv rp"R> !UڲD0O|Q3nueғc7H} g/S[ab˕iX!> tӏE\\LDUIUeAAAA! f a^(;PG:Lcuӌ$#~\5tDzmc V`iv -ҝ__DL a>q^ FPe߱J=7ɇ5 ʍDHdpEi-/fX['~xz<g`:*55tOqɷLxi?}Hmwh ӧ2( t$+#m{  FLKz(`eFXsU=-X24'N̤4G@9 !frꭈ܁b*(($T|է2_yU~XT(<1 +RA,W$SqG=s'x|$['OTO `[.Zn1z^ TPPPP|F7o} ;:9 wPk^`TOK2g#4lk@S'uhO uNN[;حd:+HFY@E ISEQQtL'8̑ #/hCJq#&jڠ ; !wP`,?B;罏gԉ4jdN]][kkȓ= tS1ZѾ_F uf&Pv> <$ @DFl QԈy|X Z5o)m噃.YYӣO:N%qB\&8UM6#@~7U+((ځ:#Z\2uQ 8ɣ Xu i}jq!Xlө"_ڷ0<|4\fSHiVRPPPP+BPxXAdNӴ8K(}j>?CRYbG9@WWҵN]EB12 [ (h)ЭA Jrg6Eb2/M>[)uІь}_Mϟ2SbQ U *RlnI!x6#a匉vn9#iM!ٓҶV9'}rO׬)xso>4Md>hFHhj$˝IYp833  ؉jTPPB;ؾd$äCzyK/M^aЌC}9UC~} 4u & m=N\BB R@7jL^z:;:Stͩ)V銄DPM72IԵcDJNit2 j32 ը/D @ R#Rft ͙0R's^I|j?"\PHă*#$YjLT-&봊}уl\ kp֤#y%whCb3f! [7 ʼbec̦iͿF%Bm(B : Q!ЮC#+L5#A ^^~^X]4K$ Ȭ4+5JV0 l.no2XXj.{3Ph,]{o({|Sz.~<<=Cf|muL_أ,_Z?@T@( TPPPP|BP $j/} A bWxsI7w|ڛ/^S,6/e9M[(45^sIeQT !<|K#(e:OԵRZ^C)O}}I oZ?'# kp_qXY,;\=Avlӏá}p$y3n^c>~D'k4=S=10e0Kf4TLڄtM7en@ R!9 3,6]=)$tҪ%w2]gUB^1cG/T/ FS 5ژYtՇaWPPQLL,,&2*&r$.dѣlсЬ:C=I9|VyB'Ѕf.s\ZRG@N@jQ], -vIj+bq#~DDqݡtDuǟ=AB%o*RJ˲NJu.QB8~5 ME$Zg*:ڃz?{ |*O[jKA`mΜ`ԗapt"B3rɣd!X> {BݙW1U+OkfD'{aB 拟~7:DfT`{*3}*Šx:v8рjI]Kɓ-z?i!?ޘ!o)70>OPv5Y[Gf=Fi{RPPPP%(((((ft (("1W$3efțCrQ#gISvUSb҆ 1;fK#!$N=`ܚ`SԘg#7Dfl>qzu` !4hp?6F%q>?imwW DVPt*<3v63XrL{1m09f.MҵtX]KOU4)ke +W!qĻ'Zs$B?BȒH?}:m^&JQъޅ̷:"I2Em_I#s5FDTvξaE_/`ŞP'hL>Pm`[NN‚IP#vz|_-.=<=qzh}~}CxmC Yyv}|y0dC% AP"e_)(((x(+((((bG#DE䕈l N-mm,Zs' gטO{ə{6_#~=}kq~!׌NGV^JdYq 5b U@hc `Wٚm;e4 U|)?8 `ׄ`gX_eX^3p9*ZK칰gm§t Vh&BLh2))l|X朙yջpaR*W”BcI#"$?CTk8޼ >_Tjid?۱F?: ΐ7j"!2"yr9hHQީuRZ/)POpҵW8oAuƂ:X|"27aYSQ Y ~g|z72h r:3s?/!hdh,ϗ+?p.ݏᕘY9,*((((xn?By5p!Qas.bR f-UEg}X nn4ӏt1o=6l{uV9^ Ӱo}]$jfq66o0vs2{.),sp]5=d^+F,'鹧qCǷ'o n}k|yqҳ>LsP_V""AD s,*((((x`pmKFB$ b$G|P͙>sE+tۡP R"Ñ1(F!1RHӜ8Ǘٍk==e)]y*h!p.S[Ivl1-ϼ5q霬0A㔣pl~6AxVA,xՏ{`ҶqU<]B\XXHU5T: Y'3}1$ #`>PP#Rie.oFA~>[XD@?BAQS3ء/&-9t!ϳuDB狥0I!K<ė8ul;ذqtq‘G̣#IL:K O P9E3KbT<LIg|.[oƭRՑDxgɣOH{QA!M\_Ϳu*;ETm:Fh@3Yl*L#fss!0L&5,M%yl߹W _j[2hyPŊ-5sme_~3O$h|i/LthJU+U FhNg˪ށH5|~=ITǑ)F) _AQKQWfϱ=˳6~|r{g9lmA)~<=UunVP=ӂ 5 9O_p':=Jԓ1va\w&Ӣgh|qs}[ }R^3/O̺?(*((((zqp?!F:3+[),A2_= Y?촞@FC\˷6_&5Jz|n\m-mmT5x5pvmZ_vV6U"1 `$PSB*u"RU4Y͍]-`!z~Od=#-O I'7Y)b(̍ϯOn/۵pւ _['ޫW 0@H!pC}{xuG&n8I$W:?g_/?W߾7/??>|?yИ.IAAAA! >\ s  OX[#AɫR^C]u&4#3…a)ZqhfSUt ڠH˽!쟾epݷ)Pb#(BGm:?Y *ЪnfQKͦ6<]x>fׯR\8qta~,h4ިSz\J%TǪ?|zwg6ȓ.]7l%E3"ǔ+`y̰ !3$J\?L"}0~) 1~x`{k*m ~ZsCw G9Q<{A~SFFLs/ abfX5TB|lm4L맧nu\?;L`iuqQ?LЏd&-[e .^E~\W@98|[xkf(5_#5% gV):ya/7fk@/j>vW"f#I5TqG->EUEc`6@Ӵ&:Z5tD3fuRifmw}(P}z]A>S_1ę9_? hhڒ?DQږeM!8vpoOO~TE .Ʋ7!9qV=Oj$#4pVB^`yfL%1K=*y1Kc%&pCt) .}ʓA5A{ 1.`Qd1˿V24uʏikðz +g7NOc*lwe')(((8U(((((x6ލKPzmGҫ\a$S5IRβj*@MYv?_nS˵Ql&Z+~J;U;;EN5/@TQ@H_ښG}͟wS*DD\1K&H>Rj6T꠶l _J^=Wz!bs9WJCgNc4$gJ=%So3F i nT"! L-:"™~+Fh^2> !`AGԄfiRKgڵKD]{hF{(vRn^gZ{|eQ.'2m9 ႂz3q/L$P$d CVG S8VY7YJ= 0h`u$ 2@Qmll"x;\vb&D8Rd0&҅u^֕;[/E֯5YGPό=FiR{17kuv6'PָTK4d7@QҵiUm>i?4m0P\zҚU *Ƌ;y7}+ner \uXYԗc%uZgMt=kU?(RW*)b><3'qecwtu*F4wT5{$84" @gڠqTC$!CXԄ1t3Yܲ~ܞjfW*2z!OŸ@(Д䂂 19IুU._y3 "K,f$sozF>RDKY7ڧI:zL]xsU')\6,_ "|7RPPPP+`_3+okέChGǓBFt,f.Gu\.4?)af3zSPnM|Hpބ\ط~ߙo}`=7|_gۜcC P[2()B"*fsqRM٤NW/YXsAy^H~N]/}|PMU$oIs.1@U%o.# * FǏ}i}s ~`{GoPSuFJx@7Pu*&92`Hjwq61K~!gD<ǀ_@AAՄߜ}ô04bݗ @3L>Qy ,}>Ia +{,MNU>1o7K/]fgOǗg*ꟂB\9; ȩWi!^)[9£C(2pNژk!W~{|;@"!|Pg}S~k63+O@eTZ7Bh⍣?~u @֥k TB33uPlBOgPRMB2W1^q#{0F:?ıCSճĩ֯mږURUqn $(rGӷ6鐛X{:T?FbJj #zў0n^ Q1v~ȓPIVb6x(wPvbxz{BM2~MpQAAC5^lvQilo'ww} V;vS9??pk`uT[;S ~QLcll"QƸQ'<ڰojDcG/ S7m=^~66nFiL0EPLbڲ,д~YjMMY8t#uA܃)Oѣ(g"5bR̨k%TM'ԑV*R"O*DD0?ygzjn TY6c@qx_Kꀏ,س}.2{őM^*kjg?rW,evMlzN}3Ju>:by $S /{~ˌ]W PfXQ00zAɏƙe#WcΧ Oo-;vC|/?ϯ?A):n?<^vB\>lNMԙ?̫`3wyh^m]2g緂 8O.oBrTG#ќ%گc򧄩F=wZNϾn7^bΛɐ?)Ɩ܈ MDhbiզgӐfZ׳ ۹_붔GN#gN-s)ϡgzV#%x*"!R-ӖU)?T-E+mc\'t dݧWcR ?R"<#H6))XO<珂IˢD`><2UgpA}!9\jT@_ TiO|y{4QTy  U`SqF2'~6A>tP߶G ߛ}#z T:6Ox G$d:<" TU r) .e Pcrz2HI>hUʝԕ0!k)/f\{6|#Kï4ad1~CNA^  ߿jw:EhNrg[aEB@hV%@yLhjmwK_[/55vN9ʑ8vx?Uh=V#!]RBe|B!T!:ď}h[AT??Gz,>*/?̄h:鷏EγKQ$Qԏ#lrz*=7] u{$* $NԐ}d@y?sV|AA⿑o~4x3b 2$Џ :`CbHr!0ܿ6Dg(ߦ,^_4>Wzeȟ_5=Ke((((( K}"OVt^V=1k$Z*8ē>q搼N]`>R]ͮjLGʬ[*O yhn=,]mThӱ#aۻy a 7EWQP+iBROV TLkڼ9t.iz7x>b}m8w2kKcU` M$P*;n~2֤R~U+!SxG96zqx&FAD)Gkd%KzQVٶ#3]罇JQ1FO:ؐ!n`s m`xcWDU5p393px(0-_ $?|y2Wښ@qȐ+Ȫ C g0rK̶3D _oo _uু=ew(((((PAAAAŵ /ƫ|r_.-OOzo+~ȫPfw2;t x3p~9J4wwk?뢖Hu Woqqbbko#A.EUTXknp}wH5*M#hFL5 W%pt2iF):[︓n8\58'OdF6TMKV*|4)U"v$ʼn۸'nU%~1{-Wtko5FkK4A7ɛm.̓|țs-:&І+NՏ1I@C2ysh}ϑ%k.|@!_b@Ƥ PPPp *#A*WQd t\2 c}Whz=  x ʒ?]@ TPPPPpxs]ȟA%а9/jwDHF qCPOlbbF?MyvxnBkٯDJй8!]?% }g=Ttc3}w׾0ieN ڦf6!HfZ'⬢ikB-wf箭ԣՎf</q1B98bMDA9)< В@"P+A#~DgP@f6e߾;-4Y%?}xիS8`3nw'=>f=nmRrʿUڙԼ~QLY~OA*$ y6E(p9PPPpLk\Qi6~D~8ڃ?"=ku @8O=8vȏaX;C|Lkws^)M,w*((((4dO|PPMlg쀦kW,Pwn2`h$Sy8Oϯ1G"}xLBc}kf?re]Xr3V0:&Xw7~n#W9eڈ@QRY'hj؄_$i1JXuuF۷yF6n^+޽{45Z%i07 sOt+S7 h'hۋmDC SGw=qWgʭ/5k>pzb٫2+ty3OC~~L#OЮ']D$%]iɟhjtZݮ''-<G:嬼:NE'qN\iHP{" }: fjaBHH%KrQ)Vudxf͚kgyܽ:ʖr( 0#2Br 8Y\j,V6{L;`/ )A/Bz B/!2 U;~=uiGOOcZȟ~0~ ~\a;SdN\&C:ӹW C!dh5tJi L!LCM[3.+:b9݈#AP7v$Z Naa~> ՞pf:TH=3 {-uNɿϘÛsAD= Ry"beOlmYRD ْIPiˀy"%p$"!bdNOUM )/RiM͓ D-m"(kR? :`,~ju&gON_o}rudo |m5H x8v!a ~ IS?G| |1zŃJ\puA?xu2}tzˇ=$WǬ<},GzouE,}'S_PPPpM۲ OлB(A| u '/GDU}wkM yE{Fc&g'毎2uYR1c/kο?d4~ ( a1F~;?23)~TjeCJyɮw?0||,&ϊKH'94AjE#.UH p#pw {B:P>9VDԱq z: <|oeW9>A+9ہg^PPPPL\ ؂ 2C7kA~#8ʓ΁.Hb|٤'cϽʟrsbJd >oHrJ=h=;έsaQvFFSZhJ t@`>|xҗF ؗMbc5c#J"x4 9$kfm]PQP̴\"|@e^&(1sMŤi8if#UhѦ35fcTKrRcԩ3콽~t[X{U@@@*xh*eCz*Csk9I8#x$/{ZxY^QK{=&;o!#6O^դVj=Qhؼ>j#z1mO<qőfPMR`o/|EK 'A~X*v.6 $CHyS,HD X'!!?}9U /_ ׃6L~?WTT/n:g.dyxH#ScG2h5?n"5~KO>Ge |p1F$ Gw>sdD{ۿ[-R_`49|n׶~y-ٸy TzCI僠mq!Eim[etN$i SED'#$DaWwrS95iJFc"ǁSύK }OcjjcSY` Z^ܝHE< W e=+P (z|j L0` UNˌ px;<\AAAA>@ˆuą_=NO'}q(=xARDuK3_`05sz~^RAl-g߽{g ᙕpj&@[7jOa┾J ŊB~]( ǟ}u_-c]1dN1#S!@bMHK$D"LYSqZyS M䏽ɃEpD|"Q?B~bKh-;?zfuW_Xz92V4yv! jn❓猄sz ;J vm B=_[4ls37pm bΩϘ"['p~O8 վ_cۜPWAeHEd­?? .> 7A!ȓ z2 2= 0!'w.Dit!~R䯹Ob*i< ۺ ,z,S]PPPpe!*(((xCP}!CH;wD9V/L`6ogX dZ~΄Sj{'S#~~^";@9JJ2=Z Gy YH7Ow~ijO|WؿwEݹogfv ӛGnG@fJ@,812- }ė^;e40׀tg-b# -jDP -7 MOԖUA`I! U|Hu3>DXlWK+?=x[ܙr:TĤ: T7,y=Ñ=ysyY n7H!r$ } s~Oͫ.=ֲ瞙{gtfzg:SJ[JKGIkUDDP(Dcb@5 ji"bRʳ<,}vJ[Ztft^&ϽoQXf{ks/Q8!|?"FBz YsD][^+_P-n1Xߖ˱c4?Lϑ[n\&+I `/CܛOmeHcmodNFpP~A`oS +#e A@@eyȻj Gm,;c%|ʐ>? ݠkg<8OΠ2o}LNzB~쑅֫ĿC5 pP!k|@'R]ept%2՝0 ]hnntKZbvٙ۟Ü SdjJ5>2%'ߕz9&daEQV62mN%fJ{!H=ߢZS iYMrrه1hnsh:ʞgük+I`,؞"y 8W:&I~;Ә`I_挾k9&b#Tƥ0ŽHqő"ښ{ t SBz"~=VgkGdd#E! ן&' W tWs)!9)@yHHfc틷ܭm4.Z~2M-L)pkCW|$E W$jd` )΃(4ܸtϱgʄgͨ$@JR\9ݞ\_eiTp;")I)%˼MCr{A31ĸ;)E 1 \XRT\V 9 zb)|Xl\%R*f>PRQQqHx;+Vn+Qz]<`ЛFETN\SwͰ'ƴX&eR`DϾ'Ԉ Zw. 4@ve>[QQQ#>^PQQQ8x;PɎ6UOE^3v]س'c$zǍ+#fKixbmklUX @غ%yh PH޹p;>fU1]xm_<=Lن@\v3c!/zw}-7ܙݼWwoIASpZR#{|\Au+HjcD+m]GJPab^wz{&9bqWr.FL:x N%'hS!^b%(})~"ԶXBҫw#'a9?#y\LWwXT"%# 5%;w e%q`o-{SQQqb WO@.Г<E`Ď<F|tt fgAJ3$eR܊JUTTTTg炮&Kjp^>YFY!GPض!4.k+_'zv>هS_qRz^Ob3z_>:ȠW-J;IiNkG{A2 @AWlh>u!K63OQU5w9I-H-{/oHw5wO]wo>{ݾ0c4^HӡLTCt1/}I-,*Foy#D)TSyfL[^J?M)Nm\_nݔA茆$@ʧq$`ď%5αg}|CFDd=Xc&PDJ_ҠJ]`1I Ԡ=ŦK1佄'O[{w냪W< f'LEE " i6R\WZWsW'hw 4(οJUTTTTc+@7}'p$y1ļPml+bUJy">cbqe"/#żq"(Al=#2a#l]/=?s9gtKeπa|st濜ЗoVy#oݠ}z+<昈d2khأin[??\Hw[a<c#,Z$kP&v6t龣'>< csG(K YTKᐮc !N#9N?/O^!8P.A@ۃ˾Z)mG@Ljv TCϟ#b7;d{IN:O dq T2'R{Y_`'0.~sezK Gy1TW1 7 h@@{2CddJxps oEEEE%*****G%dh4Oby_+Mߟ䞾$Ђ= ȎރAcpORAŃīWwV吱PlL#co],(]A}hZzxv CG}\$Q=J*c5KC\ wK  E$I1?GLLta-}yuqtD?qt\q֎"BsQ9ҶYN<4YHtAsplɒmlI2(ې(oz%{3 !':kPpz("6\@5TO@uXo>$XOntT\vR-&!R×NuI9*A~(>~r#b5H)0ϛD !ȋ v+ȼ}IA|FaxLX{3".ah(Tk+****TQQQdG,# ]*$$|)8%g{*$Pl#c"qCׇSTPl~zxmA &+O]O"LBpk۹ My0ŬpHӛrg'ϸgc$p$'1FM((M%k4'&yYn 3d&ɲ1l$"*s8Jk!]y[zz~G늝\t P6fѵ-xXk@/{sgDJ ˸/i=Ժ`1ńcN0/|y>3!ah-&J y}rQ`_E8Xb7yIDDŽpp}s dh~_枭a~P-ZQQQN}BEEEcO%;c~`~8? 0g*K7kwj(C>]?AM{es$Hhۯ @)Dj; e36 J}LoI@@h {l)2QDEQ)Ȕ5ܘ)]$2a9ʂN3E#K& qmw"  ʾ|Bs[ȡXjaBBQnWtǬ"5EUOcCx'ZDvH?ɍ_ub-ljưB!?}*** >qhNЬ%o4ЎX3tfdW_%*t8 E#'@(\&ǀ Ío_*****L?wHHr^.Y#l3O{A왇Kv<!@!4t'X#ʲoHGУjX$O~@ߘ J)yͰ}Ϛnr}& Cפ[gn[o Uaݟ#q VM-hvi?ϗc v Bw]j7zV@ߘ#BYqw~ZνgDL"ݾ+xa q\pe B=s!|G!wR3U6FՐD^k򵢢bXXP roH4zw'C5O`<7?>e}HFQT? #.;;;ɩ߅O(TTTT` "tT>>MRM&|R6.-DdO3 R@()fP7A:mQ jcXQz1LrpCJU$/"$V^l_"6=}哟41 %=XU>{NŜmnFwqЧΒ .-%pskDn)9͐du.;r/fhU[D ֗۱Fznv)6{N~䵮b44:qD3{,J,Jk<"ߩn7ra! Q: !Zhժ#h4|ҔqK)qpj  N 6pɎxPB׹~Ae}Hd ?***T?8> d|a;!2G ' FFD^ԝ @[_,*mk cO?*****(HYU>mm f2($fxM70a0 ,N{aSxE// n]pǫPrJ(;R492Ϧ(k4/GOꦹi㉓ܱ߉ x'iOs2a!sLd'Svc%G(`845@#:r#)NҰ9Oz.yIwOoM2SyHT ۢ $PLm5y9~X߂lb9nT^"'ߝZk맽gP~^SW0_ҕKSR蔒j(?S'=>`R&UTT8sJ"'1ХW :iW-aߪg+eޖʹmݨm^2{h1VJIÏJUTTT<lQD.^@DO?/ OSaOuA\L++J(1qϵY.%Pw아FзStz7",;zbI/f_naM/Xw %6~~'!BϿ'&>+UI:TndKs/)G))HFX*dDu)۶Y%Ҧ@K 4ӱܯzn-z|.rv_nz|\$jmK6[\ Ī_9g*#I=\OT} "?(NTl-CI̞Eҫ{$^T÷QgEEEE-WQQQx"Tw <,6O8 lW\3;^_ٷP%T 'x;jWz ]i6wԎ I@!.!)qPի2be|9#S]swkqkM^zbt )ƴYB}%`si0uHigs z 0g k@.)O=t'޹mvUewtNHBp ` "ȠQ X(xtDeHAѱf2ZhAF 5ܽ $+֝;]j?筐4ݝTyeu9g=;M;x$0}rxp6MtsBP(P(tl`O8y}} c>q| ~fhu=Aq"ژ24EOsP(\P(0.c 8zn\Mdl?\PRHHMB",)᣾;J  &'>H\7vM/nJWf߳}Yo8~?xo"Pbٔ@;B HLG\! -5'PგUFvhȑ+-PR'9%0UЬsizh8y3D}>GBdX"U=sGCIedYNGV\ )g7&~Ws Mg%Ta찮HAQ?-w$yw )ԗ:SDŽBݹ*T(53糌{7l z6XuH7(nvkw_%,v|x q^_)F'~m~L7< x#լ~BP BP8p\Bp0R|ԜSj/ϑq(фDi !h!XL}K>ȪC#p"Ջ@/e$nqE-9yBGeLrzZұH_2O0KrusTX 㑿ώMN|-Rb)\gQOMU S>;J6 9)9εY+ ڇOuIVrP8i*c?Å=`*,PzwB!tݍβ``1?"odL( yL׿]jxOp BP(P(>[8RxYΪ=MLjeWKB6'뗾'^盓$dcK% Èط,Wq"2 IPdn# nԫۗ2|cui'֋|/9d$? A2B@&eA,` +&9Ng9ϽSU7($AqXmPuN>n#|8~a`!ȤiređO |51+U)=!.8ҩr2\+~ZV۵mBG40Ԑx!䘣) VKW? 9>V,뀏Ha> xp")ly,[dM1t+! x2p1pI{8DC7QJx|V_~2E?w) " Bpag~0e[i$;;2"7崾 g%ܳ.FTA{tqeri+@ w¿BLE:!1}(HJ=ELLvͯ;~ p~K^N]l$> 4TV %4!={ɍ{?g*Pr*l_X{jdE#QⷽRUł8)WxF${^3sT^FWs2r4'[r2<^FkjϹnL#I=AKdyk,#=k cg*x z=mXA(:gxuS2>É〗°w|6lrpa6w:o<dc-Or. `N{Χ_qp7lh7@g;тSXgnCug) " Bēqk3yB~4 ?NyR<7Ϫ*eɸrMNiھh$bwI:RN$ITO@Nivj%$v61"Wtڍf?eͷzY6{^@Z{bbr`{Q;O dtFq"eUD9ڒ$0 XI˔zSȭdA@K@v T B*( 7Wzf\JESJׇYI18Ah!^$I8\wϋ5>U>y CFp.NJ0 iکd‘뾗4Cg[|ap^rKp60[#lF[.O I1>p'0u=sqxn"Zr1&kwK~.f|nzz)cME'u) " Bi]Ѭt!2N,o6ROUɍamD 8&` *EzMa#anG7[NW89@IϸNhX8KPR1/Z}>TfG3 ]&Zke#Et*T%Qո8e](37W9`S_v9MLL#LOsW#> G{Nog{O2Dݤ+|nG -VNWۈ0{;pmzSY W7?P(* g% d?GIl>å~I+)$P$.}zIq *ě˥ kc,'bL]$[|ʈ3jc _;T5$e M+!4*/Ia{2R'}YV"W/Y-}ǽobRpā9a}>r(_? #Lj&pOAX,+3 ' ;{X6]`ǁoϾv@S[#ϬhZ:q'op>΢&πw>FK) |,cv;PP(7VEP(}..MpkGRdńx%ȑ+*,+w% ]7'X*1IA&Ĺ# "4W%I0#n }wm-/n{+ 6Y}?yqtO@3PIl匲T8"f 2lFP#vK5N](+PE[H|NآD@r= yQ>d9*h*:PϗIQ9_cgPC{YNQ`JEJJ<5TJޏl\Kͳtpg" 58;[ X5w肒A~7oj3^'{X@|6y3F"{ksOgmvѧ~߯o[3R}XƝH}KG- R ª8xf'(b٠ldO:S~>-eh.7_.)d;GU/h٢%R0S/H/)HGeNg>m/XWَ˘O޸fEgy')dcm%X?҄K^T0I:^5hX4FVn1_]-* QA;dK9S s14T!dGȖL)xF2!oADDQb()e-e|ցg?hvf~Nȟ W&jd:tn|AI~6ғFv'z}bg^P(@B9N])/$)A2Y+zUhN;OirU粃12nq߆=QB(%#@m 5ώYKQ-9 |*&U5Χn|;OeO{NDr[Qؘ\IҜ A:;.h@ , U!yI6!9Ϛ%-{Yi#*#^$REHF cUR-LE[L £L9 x12׻*T}pCIWehXjDb(e䖐e}\)yu6"7S*!~ 8ı;^*QͼX=? zJ mHK˩zK]A+@|&!YG]{*BBP(а x JX@9/qM&>6YY •aP)^FH.C @Ǩ̛&ǤVl1xl'H?/GFz$`nw=.ey≿z襏4LcˤMBL-(3\`D-_o&z W)68%91&VXP˫B 9 av4>TpVGGwxc;j2n'#x1,':4q\q*t%b:XNs"tmvlF{kxe.16om/n53C_HˀB)t#\B&/:=Mj/{s$ʟI?d?kvm^[~D#"ٿ*+FJ$M ~E*A~tF u`r!>M!#>Jv&h:v]UjU,H2Yj|LzJVx%g_?wX>^,CNogW|#DAKVu=D9y%v0W £P( p6sN +>D(wjgIuT,3hRrrEP$k] a= fFBj]7Olykmτe'm\gOUrE#IkRw0I%c:CQ?m>$6C ˤE2S mÀٻK_RơY< i)0I3s&*rW?KjiBHcHMV\Q1Te ,Ƴ@c715tOO(%crRP=H3z`h2}@g  $'F~,^ԙE>gPLE!qw=a;2|siyEg}8#c͔@'*HC@ B0܈ %+pWUrDtTF$W4Bs :mW#՚& N5J0,Kv)E)qlqTUIKU@c*G=*5)9(baZnL@+>S(C%ڐ̍Q2H}fОKTyV: YU&zUOpѿ%uB?ۍ~ 8~x˸)ZApX0Ag~]V B@Bpax:xA1(IK˞>% ɯYy(0::}؀k >@10R'N|ue| 'Kykٹq 9sƦP o^j2&t- +X sP咒 $L^VKd/2 K(Z:3ƺ*"%}ꍴ\F~"CH :G]zuD6RGITx x~ Y?%,F 'oʻM19X(* c%8I0̮sdURo .WeX c[rao/;->憎L`!DB&z96wIowZIs$.y;伢p+ H4%^[ۃUh,@%!9I~̼e>Ԉd6"4z!qM+>g>Y3${Ϛ=QOKE\aCbFKƾ?xQrUW1y91賁ŘzC)6QXQ\m7MEp"xWS|W(qoX jU ±WU ±rP,Kr]Tּ|v\`i"9O upxFnJ";YRI/eQ8 $ZՃ$]ۓ)cD&>썿U}[2|c}駼egcSJu(Խ`%4Zhlп\vhP(] ?Գj$ 7Ҝs$@CLʞ)/ %_2?EB~}c p\*K$i\,#Q"xa,MJ3|PnLzJ\ Q "2W9e9Mq^UxP~Xq@KOcS޼$^ɶ S% 2ǀm"O`k?.´P8| #cY ) c@TPP8fٳ<*%(QcNHMibI %x5k- aٳ/ɘI(˂}cעyx̜n*az5>eIc˱8yQ4GZ[-O{z}ŲX#"<>/c!<G.ģ R|Pro<>z{b@~hU"(0oG;57TeLc0Gl#$뢿_OzW@`ge'>)y,n*zQ6`emkP(P(<< K0;1O=j1~ ~_W@7k]?9$jf`GX<[.gt.s[nퟤ_R I?}37_v}h;~ ,c޾^B+gDz5{2]Ba&J }zK^H:=t{q7-Nb$qYY+IÐR5ARA;u1IM wG{*zQg? |)ZY? " B᳏.t\A@}kΚ!5w6XȽ^r_H"`XBc/2ٹvwR!gXv'Vl7; 9Q- K o~G%00LS̲[d<,3o-ؕz1}U2 {MٳC۱Z8 {X^+I?j&_P8Z߮( 3 qֻZ&/N:#UAde/^~m]BaUШL"sE}!ť[/{UL&`(`=q Q)顳@I-.z>r2 O||8><6%WQPBL&ؽJ`seo@?U*BQK^j0{*tBBPxp*pp6(r/Q9J̈́& =Od K8zHNu۴e1[CH)6x0co ~ tQ"^o+큝 `kŧ\8]˷P( MXd\Lx>%e}uQąl& i"-6x&yFsF'/qR4<Bǜj}e sJն*3&T‘{&pNoP@~8p+|BBPxĸlbP#zkI !22Z$ySU&F2:#"$1..Q&@)d[zLl$N _hKtlo>x9`,lOCW6\TPPx@tj"ϚN.>DȐ J*A0dzUɈ|"#iJ?=qGU?tPg~^$Oal#ؕrGL6u%>nZ Y;V ^fol+|BBPX4ghUM]3C$[H$Ӛd&ZP`};;XBX H=J!Ƅ`&cyqWeρfirƮ˦9Y|qxKϹz`z/}E @^U!"P$*!8~Lʼ xUe*]P>~@HxKL xrJ@v{j(o?v]хXmg^, xkY 777R(?QPP8qX*AlL!'@Ҍ}l-I8nybXoL7o?1o (P(|%)* CK;M> #O%^Grg&qX61iXGUX:rsã 9Um UN Տ QFRsK _Ww/v༦y9~1^Pxo+pWW B@B]S^;Lax vAb4"{ŤAj԰ٓPᴘpGLj$Y4 @\ iRfTPiR#q]j#?{gU}ŭI3IA6JBDA˗QAo $#?A"DeX:ښig9w'nLu9{vrƹ}]ܱ7X[ 8>៴mrGqH/ È4+1c4淞5-ńBF ljçZ6) ~7ŸP&P.@)k%ѳo3"<0' f 5Y~"6RʆcYs|3.3xx:ۀNf0KtUyHuo:h @a2 ;)һGbb,b; G6F2M3Aل>EBA ]rqqWCOEBxW ;rfKTdؽ7oI[K#QoIv^6Y+H2Se~2MM.=eoky,_so粒)4<<ѣtw;9smyG_ a\l0 'rD=E%N.0/2sDâୗ|dt <\T̜T߬1AHf0 *&4.B)OoTGf0X^?adF-nNTtGky.+cdhY-1$|n]>͛LK)wA,>Z~HSF` ?gf<p'\9mR-oo#g\ HY 7OCCdu:ՠ;͛ |?[T*{/ȣ1ct&Y6ok̉j.HI@fB/Dx"vh)p!Y=s /&,uJ$!\ r|:__sl*-LZj+'l.l!#N#3&(8# .:@'yPqXؙӿaa EMR;Qmh> r.@Xrfh;nY6 Y BTG˶CK*MbY/t=;ײpuݧZ{6$#c:Ulֽ9Uwˀ@&T,/n}  s{h @}'{4M0"WJ*}e&ז$J8 <\ >\JNncANEL^}ZK Mw-=OyYN*zEC xqQ{C^>j L[\x|lgz[aWĹLYH=Ӈ #&kb0a u8ځvpE8|Z D`"@nҘ9RnG ˎm,0Fٶ ςσKY.*tVZO8inLB$^@D~'C wK5n+vc"s"ɮ-Zu4ET ?@ߑ~? :IW^޹XYa]vf,400n4$t$(HWugL`APE!cZ7NbkԪ5P8 Գ~|m☏^E?-91o^LZTO$ 9[vav @7'swx1f"1 AL2hoBS2*hx)Pc6% xQF߼9 UM܁~Tďj0. q8e?9dQTCv?fl`v2,Ieci{vĵE82m0UUHC@߇ B^z@>$q ,3灖v_ܢ۪Be# Ar%ځYjg@!A!A}_BS5|ļZY>:Q)LL l,3>"zܾ[E4'Ld$sPR5DTѼ%\:g\ls~8%X#c?溣loTAp2c'pB?x'v~v ٳ`_={vi&вz@3_{ym`T(;P֬MZy~xbcFe-x ag.ĠEfAq6Fh=&n]L[(kq\v9R,qZ *SkE}F>eePA@ADPHi5V@lP,Ife֋[xW3E*lHmW.uM#m#ߧ.Ŝ L0dxG̓YH}Q_lN]=`trbłI)PA !Aɪ̬ۖO>~7o*ŤGŧ!.S%aji ^(Vvlv:7wJ[Dm붮XE g3zɛ"ZzcR?+g^-~Meh]P}z{. 8 \\5C ( "(E 1=ۿ8"p'T5pՌ6PVka2.wX!\Ag2 Z_L\~n_75|(:%¹YNJSۯVZyQ (&}Va!vkAJc+D"$_Ʌ3o6hNټzaB|.΀PM9@9#5% /{?Y*2|b=ճ=sXoH?s-X\ .`21 AtyfRQ|wȶ$<8\^HubwI%Q+zDĥe4%f*OE/Gv~yn9g߫w"XVdD"5AD<ڢg^ԜNw8|o:XBw`bILlO'eA[놳(A$Tv4m>'H0;8E!.H(A<|s-e?o,٪% O3W)RXF@QojTàqӅK.:ï9vo}=7-[VRƁo?ܥai߮9g6noA[c]qm1[C}\|ui*+agx1oۭ{Xڶw O\wxextCo>s:>̚-Z fi(v~͕B$X! aLB@jmk;ʼn?vz~< nT|yMjԢ-&tńg?,& "f"mQW&OؤSjAB;k} ;nm FD"H4)r}@dD""ʦ}/hDGgZ^y _#ftpևT3NAr-~\dI9mOm@&1(z#M.Mۺ.{h&_nËxJR#jAu@)q9\|&[ngi~t?թ 1P-6&1Ga龬}hc#_8͍%{2xbۂA;NAPԩ˲ Txrę/lS1䟍0ˈ>*(jc_ɢ"K@gk42E%7DETTn:`0?Y'H$(H|1H،;E*"xLҎ!%Q'?9y}OvKf&M`{ac*);.ǶKZ1S-~vl$Pwn׎-:aϘv @S \GdO#~m\tMZx mnVwl;J ؅2{W\rɯ]Og+nz̘1SN=zt]&腿͎;n0yL`*OʠAC_N绽ve5<\R h`Z/mrOuNQ IO^J1ĉ>WO?O`Q"PΡ1!̵BtyRbyD'\P}bw$ Jl ;wςXjkyJH$MB2>M'D7A`wY&DBoi Dbď@$*)eo@΍ f;KTs K9c}GHZ@.5^|ʷ+hZUo,v@"bNEZkZEKE$ԧrM [ԗ9id>ܹcRm +*pe1@04eTb sc.ҭ[WZjғͦLeۦ<0ڴigYLaԫw}@9- N8(0ӨERkZ]R]݌]_sZƈ8 Ylگc?3>LoS_9-} oEX ?kի_Y@ko]/m8QL9}sdN^2 @p gri'ƴiӹx| `ts!RSS.EtN3|Hnh[ns=p*++׿ϼ[o>G.7b̘wU(ڥ5_~>>lV==zd}^xe(z5blQgkmР#xi;JR~ÂvmvoHPsO{췵˥;F4&]ɾxMO< bE߽M8d"(X_\5Ӭ*E$ghTTAdu-qń.@4'O&hlgHdW"S[Zv;>BiyxtFɩUn.XouY B1 &bQ?^0'~! B̷s n&yOAGk'.9A8ml[G ?ڶ1Es*"almU|<7E"o51w+P|梒^. [ݻu"isvd [7CPHJBZuMFH$ezʁ6y}QImg42c` zD" @D~ Q7 w uЉ֎p#&bX;ȝ M{!LyM뛄(2E}6\s6IQ[QsGzWq#xFv{^X(hkM28ꨃkYy! !=8EM-X:瀤%IJ$ HKDv f, RsIEj^@p"AbBKɜ LDИ >X_a;΂aB] 8b)Y&ZEemp_][ ܢ3S.E=̦l~Ȃ@ +[zK Q?"R$gRQQ/{V>c9O?0څGy0`oe[c|ԥDANYlУ=W^q@P窫nIe6ydB! &&XEamh:R?6zQ C%1 U~_6oOGR H   Rњx&;:!Jz0GZ{,B*>6ؠ;`u.L5ʲ'F.c3- @fY; ,X۷Oa}c#$F<2>."fa/768HECqGRZH][('tE8>gmES~–E懲BwCDW{Q'gO+e} lSA S\3s̶%ҍQck@"5\UWH[N>= o/fTV+[݃a]cܸwy)~X9TɄDDDA([ T|1i7LqY H []u,ל4/d6P\5s|uuɩHf/q" wE=)]5k;4n4o@]WEkU-dmͭ[6$M]F)u.tc saY5;Ԃ{8 ~l nB'v[.NTR@l ũt` r:[ 'VVǢJ=v}wt$U*f\SyyQG0\}􎢁0f}phT\j=pAy H*.+@5նMe?`0hH#F>R&дY^vܡ/>n"lmxc2t6^b ̗H$(H4$kn g}}姗"`GYZj>B\SF? xvw`HT>Wa~Y9~Rm^\X34ߠt@IQ QQ/>2 @FiӺ5|&~:Ug.+F<@qMzq,h/:hۦ{nι??]#noLYU:</7o>'|f/ *y”Nb`L1F]4|g`Q!b]E W' dfObN-fT&7U$"+>;{iL42sYlv{f?FrBfn{=fNy'G~KFV`iq6WM5-6 SLcuby|l1}L'OYڃ瞹^?y8#oAöw_,X\qىtxG_+cޣJ䌒c!͢:[qB3aE$~7th bG}ŸTnzWW"$˂ 1gE-{L -*l֞ onu\#>ؽ7A0/ -/`׵}}4HTF':yATYnx؅xߟi4,7?(e5mdy"8=XM/~'ݸ5X:k'&IJ$kh aN"N0ǽ}B2DT,bӑhѺ.BZC&>nΦTAsD("~Vwr{q&5@ A -^84~t#>5h'(GK'SWg/RYk[5'E2 TM@ P+Ө&jAϞԩX\WLKn.}ǜ4aYNK~yO3; <󜺙ͩ矁UWw>I"M!DCB8=sc> "bPXBE8ĄB?XW !6^ZLp K =OegU?i "y"O=pWㆫ.[Q_?){a[C@u]zX6>zD0v*-.OR`p |^;dDSg?Fѫg'_>]`yXv)<_y`6BX+by_H7mep4 ONw?m$%o!' JHP8WUqåHs)_x 8"3L DO?dX#Bd&|ݻ N2Tskd(~XdP,v^8.&Qjy1;FqWAPφN A}%^(&|䏝J]TT +]A`DOL&HkckDs+DJ{A?D}#K l6 d |vlPj&&tmdM?? GdQ7f]QO=ر;ԏejqR_?Q*M7k4}`, 5/HP"XݑY p0НL2"t{5c)6l# UPfƜ^{s!g#c&>0$ą JD6wv--uZb2E99o[7ݻ'V!}LVu,LLkbj 3_0Ͷ." k4ț,R2ҵekJ]t{=gl3|=Ϟ{wsfݽN1 pw!1H) "j;UMeaVb](ө޷=d i=P6a30rpীkjjԩ=n; +_s78sN`dѣ^XXQ6txB;|#h? Td1~n|b|? _ m b==q*4&@fOʋ>tLҬG'.x'9yA0^;|*S^Pu(6Y49jZ8>&O%Xx EEC^%~ZXr0]]C Ra 2zv$ qA2NۯOΕgWBӍ/eu'BD.xv“#.vOm6zׯ[:9Hk,ӵO߾, 3MiŐ{*kZ8fbdGm.Ƹ`ca;&ҹʆkW?1.}%)^z0̝3 ;Z _G7 |xZZnCUpF~s% VzUuuױx  yprͧˣG tfN14bQǢ&+:1.r?EPǪ1qLp}_4ָGw Uw# F`NTDL5eLuc#>cuOh;J'o+-lLjw:M8P\:Љ%< !d;l#Z?(@Ϯ4Dxyi݌~ d{I ,v3QA#QL"KFDc")je( bR4ޢF%ĈƎ+*Ϝu۳|}{ (}_~ǝ5GJ]tlZQȩE}DŶtI'tig3oGA BuQ!$=[F ?מk-;3 ><1ʐp #<uYmckV߼?HV2{M>$rPG"zxB@GC@JIsn2wq7IY"Mq;s;Yo,&y³ϾsAr@Ԉ87#BmG{k4ȣ?7Qd5@O~wAIcoztRi Ǣ!$AP {uIv(z>VCx =j ],Y5":~(78T<zh 3uDzK9i':Z(#*$T>z=/oa&@O=9>|Ga~g7r(i3wWeUWT~n={k\r?v%pd vE]l}g~/{'|W@+}q> ٳ6xσEϔ&^tK'tI'X-E EHs򰬏@!A>pENcBM9 $'Wvi!?M`!d2ȣqY INQ޺ߔ>?8tNmƍ8iƞ=>%'$;pr:11)ܑcGd}:I zb}p͛G??s|y)glj&7oБߧ}1ᩧӟe{ܠveѣO#յN>yBY߆P=wMhlh#:D4Wr DӋp餓N:餳OF, pQOBFAV#2ر7ȸ7{8~PONr|r{%"g9 jȢJsD@ |Ѓ^t@՚#o1n rk>zfňK聴GMR+Ge}r YN=FvVYm ^gͺ ]xϿsG}G?#ny~ x({`̛8zE}&#}35 H!29>* \?cNБFYN.aQo=G9ոgd^p.?Bŀ*c`=ǽa;.%>QȡOEDv*W_W9(?'8p]M48wݵq˷n# x3nm_){'?qf\k/_^^ou3úɘ=rMx|.s)l@Mߐ?(n0@B._z-l2/Nx)v3 5͟XoE!>+j^K'tI'x. f#?쑚#]!d"#✁ծy~p}"cvCЉE{ccBo<H!N L*?탨RP衶ÀR Z|@C+tG   ց1h޽xCe4@xznoyxWO\cB@A~h[ÆdwsϽH.7⸧76fgʔ /&;]7@lta<~t@MƚBN,!8N:餓N:M(ڵ~ѣHk$9xS9 HkrA$"#u<{HGa | PpGs @ K4*9}{~)\VRJDT(?"䐁x'Qo N 4 G@zJ x<O߁xS/X6 uΞΛƑ뾀?%\-h$QQUr;ou>N }︕υ濷TʃB>a u Hh. ;ܑ*C4@Y h }|. oD 1qww!T1䡱p!$=j> "E HNeD,#-BrvZt$gpt|5{^~-^tNj?19 3mϯ{{O2j8gt[G'Jݐ!^x)FO+ %cg怣.$&#Cw\Mb]:餓N:,G*&x`9~w4G@!9\7 @rC!}@ | ??;;|;FN"5";|0s:Ðz2䙺Ǵqw 0?K(?&g@ A&N#ವh'@3YW0|0ρ_䆂ۇ PΚrA.? ~y>mN$y@XϺ@ /759Mv0: s(Z"Cn }Ŏw\ @C[!"r@A^c=Ȕу?]?TS^,wO ţǾ!w쁼:@' uU৕h-QȡE+$dHDF,mh܏Šz51?sϽ> AK騸+k |&ߩC~Iv;>,gw+(@orv;ƣ @73mӧϠϟ)D?)Z.xN:餓N:僟t  b"jc NGy'rhԛո+C4"rA bS~\3ANwoU  @?${r%W3M=?_W;aS#brC"r#C}KTg:2tB@$up<,ЀG@ A0N } q(8qd .{^=skmv8 $B ,APqޒ@]4S!iwK_N*_jySYW4iT[|Y+3zܕ>[?o:Im!A:9 #=#>d!yge #KZ9X.ՂPo> E$7y>Y<(pA_k =9.q{p^?[t@$?hz 6DެA}X؉k?vGGz;^4tRI4^QB\{drϢU+$8\|5j?'~Fw5>7t"8g^[W5dd~~'ҫ ,y׎e{V}hY?Mp=' O G>-C+[;>χ~ВW4 th"_:餓N:TL@ \ c޴?p0!裲ڑ>z纂 o3j#~ FNH8 ~=cp5|?΁A~Zzݎ/7YvZqҌ ysњ j|qo*A&-젆G{FiNc. ():Qû߁%}]? ˡXlS)t]`u@KՈN}Yuh_:餓N:?#h@?<΢M@`"CLAM<N8~;e @V 9aEHր>8ܕR 4_U/=_?.ZsMpǃ? ~L?j?X=ƽY\#90n'u|za;i#]HҒ@>j Hޤ}| qng@ |9@1bH9 bSFA2|}?j"Ǒ  A#X>ј7)sPdU^w]&(D}ErZȇ%`nNh[Hy5ޣo#?g*yBDƵ|}COӧ 뭷7Re,,߾{>O=\jcu]ojuNA]DBNWUz0tI'i:}  m҈Qo1oԇ|q)p> qB(Сgx-ߏ@܋joܩz=?m7f!᏷ |D1?R iGCC GQMvX@IMz/-q?xF4? 2 PK4eiZ~ KE@'GEa OB7D?sdCg'}L<Ϋ}'O h,\@pXߞv\=wi}3Dzc8;O* [D!扆+$D@7' BRggjkO}K@{98<У?.ߟ# $~{(,g}}mݓ?|h};Eh@\>3\)Vx tI't~~*ڞ@1?3w!GGrY#85{3G@7 B@A?䁬cyp~7A[}֐U={[7>m,>>;9=S\ dQcҾQa[n #HvҢh]=Ni? GEDgY?=N$"BN@wABQDcT+D+ù/@ A-<ù|yC 99~ 2c. i P_j()DE߉B Oth ; }?+syx{񧝅2ܓ{t2? zg|#똵e[q@]l'':V]uSyM<{ |u+g=.},PaK@DDVN/N:?mE 9H! n:5DdrR PG@*G> p=D@kTH~{ >!'cw{?) z؝w\@Z<=y i_yߜtIG; bcw&ܡqrC*,7c. Cr@#p %-ybU;=zp@ O 0*P{N >GjE1 "? `~ @ Y9F&{Θ7=tAwE"{vЎ{ k/;boB='ڮN@ pd ~4RH{H@y;Oa[=? {^(1\dy܃8 dPG=Gr!`vS!z>o0h4.i!̯ٳhG- qC Xp҃o&Rd A}?k8;Nr\>s}|:@=sR"ՈG'(vǾqQA|?O<F@ UxbTZkWr=A6rj^{^~?[0 uݦܯG+%2?Y7k6ܰ57;;/GSf#i)P36&CtK u}/WcLv=PAۊ/#Z7N:餓~~,%:乀<"j;~|4"rz;,B;"$Ggb~g 蠖c:90g]n31wĮ zǾq1 o!? ?9!Gr@ (0裏#{p9\kuw}3oާal? PP!A< Prϙ\'H8B?M9Bn 5`2#9"},fG7@*`yh@8E筷.] G}9ԓBuFm.w[v|wIO>P_멧?&!PaEvҋ餓N:4gFA>j,nE o?*#y$sO '5n <"$5 DE>#5N @ ȠwpAj Zeۂ\gگxy{|o<-EgG#7|,yx|}zH9svy= <\z bdz s:xտ8O<슱1ws;>܁Jpã@%7az¸7}wrh}0_`e!.L (\ 9a|#q|<},>*@ 5}P#2ԉ{<>\WYDN'v.c@?EVho9 ]@@ZCFzմ}$j~*Z0d=+H$[b{QU@-߾™v]r>FG9<<#bLOi:OWGs8cȏ/ =AW߈hK A7|G:{a?ơ}>y/=w¶߯v2[_VӨ:}}:gͺ:ؿ~T%}* 9htI'tJ H=8<0}  'C>eA?G{ Djvv E=˝@Pq3T7yw9EMպ~ xio[T7-x~,W@}$Rp I{{#߸f$'9i? p"#`(=!Qn5CquYMjr0Yr@ ţ3 GY}E&F4VeA 71њ?}N|,j]?ˇD5>}D\ Y?1)٠C9_Z Nkɐ5+; 3 TAO%N h };.:yg_I?Z^WOW^gk!H'BchlAM'xÅ-xe} t[nSA7r[h|뭷]# X~pJ7z ,Y4M({tI'tg5Q?=r~uAs9Ct3N C1!2O}>{oXM٣ww(8@!DjZs롏:$pf߶[Rq{~x/ @@Oi5 v!21z@pG_@'Ss@{"!ؓY0\?#r`˩VðsD"C9A }Ѹ7I@;(OF@(qA*9`5 ="b(9Oƾ@~i`Y4U˚k7WިB֬g8@EBփ,L9 7{a!mH@y6_|`c:VP1l# ˜4Q_3j>2~в?ĉ>k=(\G.F#G 圗^z%. 6aIi=)^L'tI)oEa?Ο!xD=?wY$|G!fݣ&ǏE,=H8Y. l|+ٰ͞s{\z'T<I"r; YMbG1pѣ{V ~FwYAv.{>2@?֧\ûϠDrP؝ EzT #9"ƽ==v0. AxqpC#7Nݕj FPAD0wƵ1cŁբʽzcӡbj}1A Ž__gK_^xe{oϺ tIcb͵;w I'M݉Ϧq2Y2R@Ͽ/*z}ڮ^N+u0su!m<;nz;s~9ꪛ6|\@-`?ޢ7AW~^L'tIg@*==T9 Bnmy|;;I~y{ ? Fpϡȣ9"@NQ^,@12kuEV~y5+k؋*N_a|.!rh+kWn s>*yB#Q/FJJJ*DGϹ@>9Ms{&&D ## _#z&$h"cB-Yw@ Fi"@O`OpH8@p立9Hj#X(!WZG 9_Rmm^V6 >4bTuԷMi%@ &Ӄ@|ٳT8 ya:%k9aw${ysٔ%F9h?kzΔ?> saoq_2~d6{ *5oZ-ED}a5e[o?s5Zc-"O QAM7ݮ (_B^wM2dAKK3QTl@?#щ\Z{$2EtI'tqT txh4Mp8o*@Hz$LQi-¸8cr@G#zi(kxP;G hzŝ>,6zV O}W9_LXP VMQo"#1kH!$wp1paTPjAF%%%- UN@' W?5 " ztl|h웗 1QYN=" A r𸷸v=Abg=C *{D{b}Qo\31D!Cp[{2?͗pZo[MaH?XPhu @C['4jΜ7 pJߗ%= SoU5I't/CT/(:KtLPQQOя*+2wi>  ֚Cv}%d@^ ?1Hshxr1wqoӈ {{~z?f!OZˊv'U|VE1#9Ew'ҏAܹRxc=}?A>$6ŵz P @?ip@ Yr:q'v},]?vG.S5 q Qpz⒔T *v 7GBp=DLJ=p ǣb)<!o rg"9FG. 7GkvyJrDG7bc1>Zvε'g^>Vz {HMzc' rWPRN$%%|(׾=sN:,Iט-Qh/:]ԩ ?ml&:Tdp'K3C o$>o>vqoqriNp'@a,b@5CH3CX#Fٝ\ݥvO?l>?H+(f9{>8/'jPRsYW_/II y1T4A(Ч6e*}I't^ŠDSDq|#Q!+(vhj5Q @E?Qo&AlO4MaOB z!УZ! D}rDܛ0]?*r}ΖVptYvfrΎ# V#K Ő9FA" 7{I?g$\@AHobY?JJJZȅ@"7@ph\80 20D DUqp}?[T3>ArD:;Ї|#~H4 >A=d ~UC'jB N!;":}hpЇrK4`w~,uVYՍJߛ޸fQȩvMI'tE'ZQ2ǽu):VS?C \AҷZc A޾{FA98}C@5'L1 {V?/w8oʰZ5yAU=lԛ o0"9C?xog8ԈGS @pPRRҢbOiģYN. 7Ӻp8A?;Ѿ5vO#E8Ԏ3Hȣ |&N ?:֧n,<~Mu@>fو7, Զb=ܑǽqv k:^ghZ&Ѓ0zz~D` Qe@IAkex̛iFP{^[vΥN:,ip.=a eюϭUpw z#)A1;pχ@T2BwGEj׳Za#sQ95\?Ԟ3HDGkwϏ?zcx|(\# pρ*DrHֽYb$q w[ns8>:X~]@" ]@Iߗ~éN oVψc())r^l+:} Dgڧ^:餓Ny37NuhL߈VcG ! "$9#k,= @!;b3Hc4.c5E參]?1!ٝ H~T#ðkD?çZgMb·9gϯī*F<9~xn AO q?j+|$WX7%GgU4 ())i1Db@;q$ r}P3H]?"㍂c( (Ac,=`w,wq0ȇ>\S^\o0zpA >{ U;KjjXj/Tu뫮!;"İg85cn)) 4^eEmJjJAa.xVڛ=xWw=LJJha_ :7(:\&}I't׍QhFF4I{&_pV?"<-[A(8dWlг~!>C},2=w 84#h_Pqo9.!!z?c/׮tVw;V^pp!~3}28x"$<{p }4͞="}+@Bh@9v˭~ ~^hS@r!=N{7p @1j=;xV 'Hqo}(y ˹]@=AC~\У=@yܚ+ȕWr?Lb%Vn/:/^ʵ _>|:!Gƽu)PǢj 2V %5N:iB(|waƌ+²˭II -z^mΥN:De#Cm63o:`\?"F1paCrFa =ay @#6Y A>>.H }蚕;Uy_m4J\> H8՚|b) Z"@T#ϱ}GOYsKz\UuGnVt^a;Uۇ::vܖP%%5_CsDjAƠo "#*Ӿ ?788;w } D#ߤOGŀ'> !. xP9{<=RCTG h9,Ǩ7k-F?Fõ5tuD}?T[HGH $7:A5 %-tȐ /8bX%%%ԔYVv& 9u[N:MaU3Wφ睥ǁ@ "\AI H;4?=Z#ƽbC=5Pg # w 0#jjŀF=wjo[.<١cq jAN `ltɽsUcy; MȮUwAwRԳ@+að{+ɣnS{,]S~:tl4`Cu Uwߔo_>LJ=;'׏k?A@{e p$#Sm1N  sd#  @Fw;EݗgǢ 1AACC #=*Bis:5k/~Vm!=ˇGe1ù!?R03#Ҟ$M(iaNw{_gxaҤaMwL߳kբE!$}I'4[} KWTcfbO< =C~TcƼe8{A{Fs |& > 9@FVuiAUOF\g_|'7U뎾A ]?q;~yJ`F!"rG! ϙ0Mby-Y;2xo onxgΚr.K.N9̐u>3]τN~Mm-škmkNQokw;s\cGS6awFQ8kX5 #L98{D}ڍA/~oAvgnV^v i>Z<rS\Gv'x8Yu<9b7hvD:yCʆ&Ɩ焤4WMzG4@*}I'(=# K0 b\@yPsr I;x7!ܙ( JzP jh'9|{4Zm}Gk{=E@N Yp4ʩX8Ϡ&ph/@P7pA}& Q4 =;Ξ$Z! 3(/ 4p*?;X0'^W0\C1>!ǏG`O aO}^"HL())))ƢGD!6}I'?ƮDI^vg̨8f@zy(GMA~~ d*vGn!wGAz!9DrяA3bgի {1s4qArPP>}sjFC}D?.ᜅqp䏁Y %ıq.#_!l~^@#F cǞvZ'+ s|8iA>z5'~'n#fκJaR/6gICy ."1pm}@0M%5r&2pǩ5B폀ctn iErXpGc9AyyC ;;|P c/PB"^sU8yftg*[:lU( B;}Lnmw@88,>#?1ciMnyCju% 9uhK'tZ "JZ  վv&j(=Ɇ?*~CmQZN f9.T CpQ/acB@Z3_W^(. +}nT|y!F}5~ҾP)0Ї{ht>C!qЇcLJ>E nYZa qAܳMhSH(%m+mL0|sϽP4瞃CsZ?ƍ zlVY{7M BM*@@< .!w,WKo! ٝ O@ws!py@CqOrA X>O}@ wx ɞiQ5rm֦g:p$G INvН7MD"GjTD o7Pݲ&P xǢ1C_F~{餓?JQP^sc^~p b= h#P{AεAj@ [D~ JkAf '=Ñ]?g;bO@O|`ߜ|YM/rtAZ'QY{A u}!BNM99, A ,qPpX8 dz<N#A1 $9F™=B$ @ UNA+b@˷vypGߦꪛW J\> ;:Vys-F zb.Y>tPù{Ϋ-إ~-aкny @yG(v` =@s )]?:qcőǼ47ܑ94B'H9jo4 LJ>!^ zX]@p8=3tP+}З<~SY59_ȩYv Az׏Fǎ :Bр" + 1X U@IIII BW]- 95GWT{餓?MDUvzsŸ27;1 }Ƚ3 Ѓ|D< x\Bs$<a'A8}cu<sH;LPөβusbϏ0v灞G(B") xGE,ZUqA*#[,h6iBRK.o3wGaKٷz[xԿ }uH;^ya{m g}QQrS3' 7M:G1>N~^{=xnMXu?WPÇ*k^1 G}\K@=b{~yd5"6)pLp@V;GeGq@wb_ {d ^u3eym +Nb?"@ 4j; (r xN!?'pm2;?VsoV4/B=l 8NXX}M m*@  _|yyAGAcnM,ѱc7x[vxg yЛ9J8C 7A|&9zi~Q#B 7|+̞DN`gΜ71m u:lԥ&9m95Hz߿~b@ Q6c9@ xY? {1 fV-0[А!csXq 46>&} 'H~e~r㍷O?cgsϽ$%\!>/B%•f[~#Ǎr ~<8l=U^z~OTr30ӧ_|츓=B3gޘ]s eM<; ƌ谂覛n.쬳/̖YW_싏eϛ0R:sj.}x ')YQA+PW0ch_kvwI3ZÝ~?<*L>\[s :PGtEj"?{:Y"{JJJJJ[|3LLY 1՛:W/Z\"j<ߚcQ A|#i۷#QN Qf@r 3ugi sL< Ķp-4akA]ȡٿ'^ZTKϏ펺0c$QO 8pD!N~d_W_}}5_/`W{ F͜_:5892p"-b#u  s~L q] F!iGÚԩ~t6x  {BcuW<@S1! OHPu2}oYz>9{w A!GuAs D L{q"@`NPC|($(9?]t)+M;@i֢v^>餙mNu8#3jCT@Ga#" @z#GEȳ`@B?v:cbQ!>&@A\(AR"쁰C3L=0b 8Q5N 1LC#X@ɺ[gn Zpeos [h֧V:\\gn!={nZj*g4j s&ua7+:! p;@~;lh*ѮsTrbm~^zΖ4v_|9rτjIB˿fvQ۾M"qúPp |K[If8vo<"O9 r{@)DSCv/<˨GJ [L:8}[.IpG@RǏHf0_D!|9@Ѭ9?zK%VѢh7k4mը!o&;l8qډ\J и[#_ @uȡCcN<>|~R߾h@ yqP|]u̬O65et]teqlfwN >b]A03?vu=94'RӋy9aBę? {47ٞ{:p?T)NM~ tA D8ACCg r*Dcְ9y-ٶyN ǵmĪz9Nkb4INI7ϥ{S([A1  P@IIII }4)+M:W/j5=`M @`~Mj{u"4BH^!.pVoP tr囜-zP' HI5hs~~vs]G!UtװýQ vB7E\z =?CE1A {F?53#jQ:q!Hr`P3[TO.$@]mR1oݞx) = {Il Jz#dzϾ \2^@A+u5AG5*Iծg4qqOT͵x2<~d|6.ZzF>QB}v8"47u̩B'Pu\@RG>8z\[{*u*!= @' sADΟ(GP{?.v J;8q:n4Uy6hj [ H]?!gpO(񜵸%\X ?&;9ˌqP5AOEPjwkOӽ2K\Zitk4ݩ)WL9<NE\?}" ?HBsbC@Ě yX}=>_.]aثv|<`"CCdD\$(nH#?@=$']$˜` p'AqK8XMZq<9٨=^ 3o<@ 7.{PgN0`Z n& ̲GcMp)#uBO:)U6nWqŏAQ{w6}~f߫h>|L^[f^uƙ5ff@tu:@=)i mԩ!sT<*)mH1NE#0(V\?̣?" T=#${No"A\#֨[n9܌!wtѰ9 |X#Rr $µ!Tl6psc5C.vPRRRR@쐶L7e%uKzJ+}/Y42{C-ފcSAN՛\3͹vo |rP)yv_rdOܷPNM͉WKSk3~{TC:jxP {)!T o ?#k`'8$ ? L-~^-ָ9So8@`c0&4y aO+lA3C饗^hMv|SE75sWn_jZ?T~$pZv{}y :&tLdI~A={nuj؉8;看!^tp/ H@!C{Eq"*-X8($ W%$:'c@m1@SBtQVp>C^va 5]ȡH7 Y {q-?ÙRdpk̡Nr:y?ad-JJJJJ(-'[gmĔgM^J_2LM&x@ã펾lQ0D(rXs@ {t?vptz $B=PmS-ᎽjPMf؍JW[ڎlp gASElF3 z^ ̟^%^B?R?:]?́`5@xP ~ZDuY>.[s@hߕ:#%[ۦbHh cH=B  w2T4vI/T,qҸN=\O8@ǟ\<|˃?^sCkb+"vwiL:A ž\wϸ9@gEij hA?n N &8W:d_a]Ay@'pqBI7Yj Jw fU8+A!<׹?<UDأQ[aO)_<'aR8pPh1@p.oou`lk4O{e5Kk-u7,I7>^Z|t)+7MڥW/Z_,HeSݒ_Ԭ­mq@:IM=# dG`P<5 g3@U|  t>eJ#m?hJl&. ?.-\xj |'p i&ҶnAػș}PG.sTw5i3_5Km -7hC37a4{P5p)ֶj<6̓oڼ<_Hn( t}8'v!ʞT@ z" wq鬟İGŸ8Bax6aO=X.)k }0Fs۲{2{wp-Ў@СEE]9H׺N\=+xch^[-d |+l\<\{jZnpW{ nE+{yp}=,[m=s^~uś(U[u9XDs)HȺ(ޯ&}5''@/6;ᬫε 26Zc~JrӏӫVZ<0o4o H~)'y[8&FAa #5Іm✳0Ǣr׏ZGkYcl{ܩ = HA|x9{41p l\> *%j۹|<#E-LAa;8<]@[Qplj"({&\& Xxf `S-@hgs_y6_a n*nwYuG >W P:f7.? rzpf4i(Ŏ"~M7Z">A"JGri&Їg2Q=)Fo*zXgN"uq}A72u֏Hۼi`Gj FýIBFu5q5vٵv{ʹqsߝe\F}.{77Y@H=y BhP&\3@ΕvmD[;6L;fY|Ź?3ԣ9@A9G4u쫷{c49AΨ #adMڽ1?A;8Gޢ9@9dơ-pp. hx>~lײvC->\̟95. pðZipf\K.Bp#ߞ`mƬV!ł+ĮÍ=S`7~Iz]]wDd߁h7rq8|"hVjvSJ40U~`48c~wꜝeȑ\@6.{ n%2p_ ssk g]xmK6 ݴFo@ۼy'@-8G Q-3xF8,',;^sD挢:#APK A8Fim"Zx r J[M熃8je@s!V|dF> v9anYbWmb->tP\b8At@.B :֭x؁z]Q[gԎ詧p*-@x3j|my Pʗ8K]>_on^}nƌZoa};4Fky7X,E-n7m͞/:+ twusq}<-8y߷ׁɳ`u9WT~ W[7,cߣZy0=eJ^:K+o_˙L۩冽q3/>;C?l   jG숂>g2?h#\YLtmNnr)(.*Gsg>A8|B=ft$GfAPS?'!a ;<Ǯ(A1uUg-ݖqlgkꛭvfo}Wf{,8Rl*kBe'Vj1T ZLK݁D7g1pOa|O"r֤\oT7Ss x: (lyNӕ5|X{j z;ѺA"ۺ1R)殎c{n,{ڼeL wD  })|/nDĨ HϤś{?.A8UÜ{8= CIXu;iD88&*8?J-pi0H=Ɯ5H>}U @OQODH9"a.Q@ [9! [Y"Dw4Gqo:~LfrՕqoNY~@WnG5.SED(4<ɼh_-ߠyN nB)-z s:7kl .yڎs$AZw}s 4`=s/f޲ PS $tV;\yᅗ|Mo /g-#<Ba_v}` ߧJ.!_$3k_]?랯9y{fO</"1q8g?sybo8V,L.z37Mh| 4ل{\%hS꥕7ΦNsi>޶"@GP D5iAGE ]@˥[qiӉ-]?C#!.|9"UfF D!u*琴Ù+H[ñf*79]>qkpGkygB7ʀkNZ)>5Fqtc{m >ztz] Y^u䰇{*>l5qF;XR g~Dq/s}|3xƹ?!u9@~"Q($Vp B 5t^ $G@Dg Tt3Bei>DpD& :B% ~sfl/ 8}hx>SWp~¼fϾ/t<.fbƞ[9.$tZa&/|h8=#Q؃fRьwu{iGՊ_sD 4/Q Di+8= )[>壮m8@>P9 = B~Z AƳϾ"3,o1+|FHIk3ux @!R!= 0HG!ĺB!q+8q MzCp@ ]A?E "CD qUrĪڰf׽]r6{ ġG|] jt&w!S?A8~\zЧXwC0T<^{,G { !*luhmk1i7qg}3ͳPT^8=尉3o B~)g/D Ғ]  8\־ Ɂ\S{|rR<8LY;f]Z>9thزq6^{ OZnzW(Ԟy9\c䰹kТ˯Enn6u?: Ρ[o3x-hj $?0Ԯy™AP+sσ3Pp@J(ָ7ִESVRVO^Zi-4gG2񆁏38:x wCE5=lF$u$0Kf?j'_fu{0 H]>~A ~(Q51l+?>ZC`k@E@'P]Y. AQMtstΏ]@IR^\s~wZK%-p:"GyTy?2Gkk3)qٹDQA$DžrugVpxpsD^Oh&P ~{m@=gi=AzZ[;/Fu=DA0.:$?  (r!5ً !QDb7䬵hQ}r`'e6lU{="<_ϧj*[5vSyΨ~˃2_0gj͜ycyti\'xF 3sl-Keu#s9yNEcv'0`$0 [#-w-Q'Tq P/!0e֬gPo?pn #{3:9k-kWcNs8y\{C6Y7_{ j{3p:f`;Fj@iNzӜM~^Z}a J]m_v阰k>.¡xn~ZS ߻Y@@dc+ 'Kgs\Qx'$|9hѐ@K ^|FNǏ[q0HA91pnY. rq`ﱌVt/pH`OT:k֎O)btA[+f9[  1On.if ua3 %sc5ˣ=3]ؿkp^v7W{O6M(־wbӗ3 6O^Zi-?[Ip# *8฽kP?sig!@9[k&uk \ q& 7 DUЮ-7| dZu|wR@HW;eg,Q6{gOg{sl입h@=3 aԵ :7cq.P$a.uH'Yԉ ZP;8}Iu< 0s:.E  g#mT6otT  ?txA-thaE؃#jܫʷ~S?*?1Z UB[4fU7$^Ktif$sh~x<@Úuc4i1g8] A&A |DVk jlÚO(~6!GU]lC05[g%V3 8DJ q>RQt3,a33k˟ZgLEMsR}!TpYVYK:sb^V}9ZCG^MR.|=L4e%5ôjzJu~t1Cu?/wǏuQn968JA~n *CCh!Qk{۾TB]A=? v 䌔oQrySdzgL aVoZQ,7Z%%%- :Fȣ@H5= uca$P'tUw0g  u.Dz)ܳF Ԑ뾳H@f]x9?ծX\=GwQ$s # , ֊'o-vo~V Q8k7{i&{m&:}bD#3~5{DjK8?!`M5\u^1kݽEgu3iV@s >2n ̠řϬtY@Za=܋0 k*Bk1wIf_I;6sGy'g^C6?| wgf{SbfMe Sss(k32<e:h9|p~U3Ј뮛x9F^5s9T6({Kƙ>5e%44?K+ Z$D[NOiQDȣN >.O \4u;2kuK-ySa~ok;Cx? A]k2aOB"K@?yxxw !U \>NfARS`H[… HsAZѣK-b9~TkX&1:Vpyc)"@pAkt0@ZocAsTڿb>7=<\Ko9 %Ni'ŸpOŚb"r#{D:|(Ԩ-b{\!'  \A{z|&M@hŖmgW(Zo*~?뮻Eh3_hUzZ_laV[G [hyfssh.PӦ]gyk8y,1ANq.8;4F>5JaNt]{"艣Bx9P! *:~ 3W=UݛǐB7]t <xb#{uA\Dp@wSn&Pr%%%@=*KJA*B =9{=?;K3Hb8@9AԹ?}s:B8% `!à3}Qk-WA:qfy |J(n-  i;8z?GO"zb]w|~nO4($! Kt7ӿE2ڠ [pƛZ'x*@& 5WuYm8Z8![h7Jf¬& swl/ l6Lncm`'<4U=uq)*P z|q|^3P*B~vJ[?L;>AgP1'^fµ-(XS\ zpP 2RA uS@ HfQ|bWa r@DqGZ!2ۋhBpFǏT!ݛY@ :~o< uDX"D > |ĭXH:X?QW 8 v@zȀPM׆ X۵is.?@Lg'}C'QH 4]@txO 5uy'r gGbvoR_Dvz>U>Ͼ*(637qXf|@kt-ntd\?7k\h+n :=lkyo|b J*A)y||TsqT:hu?Ī4 G9m#Ǘ^z_}/9&@̿_{8pI=lƽ>Gd|o9琺{*(i'B RE0\ !=ڽ-W T^aiaX ?lӨ597(vxd$?Xz8 z CnU R\hMAR'n93s=!PPCV 8{,hՓ8iV?*&hb||70>0 \sCt3HZD4z yp<>"dsLp_ 2H6_&v:⌛usǰ`p =nyl^n7sS|9O5X+3 _s=7Mky|xLAsqFKeyT .4Mtsc> :iӮ|=1g~f A&=S7c6@v^VAFj gGvq:9v ڹ^U*]P  \J!C6~ vsp%@kۣfH! ?+}\jCq|./\Zݎ۞vNI3s 챼}4 )q%@9ܫ{h| )5 s|C.ߊP^`(>>U`PS-rm_jԔp@:@']> ˧ S|$'\ a fH^Q< Sa PC{cM[µc9KXWsW%\ tN:vϣENxH\-p1[Q+HE@{ӺޔPRRRSZ!Volm#G?`k =;^?js'ak)}KTG~c7{Lp; {mkи.߽^/;ګ5]9hlgiȚ '. wH 8 ™=mBcg#A msWur&UYzln~Z_N&%UV@i}@RsLMYui^ ֲ=n`DRkf|diGu0HtA~6Һn1;r1O |KsDEs\ D~ 9 }僼ME|FN FT>a(=:|Cp |"s -½GwZ%%%- Z༟x69<&s}L z8H: Dq_?Rc7I7q. 5'ATh,J!N Ja#~Cy?ۭn!Ƿkj<>6y?mD(;( P4-j13tF)( HVm]B(fa+QhQ"Z5%qePM^8a"xBf+Ǹ d@ |o&~X>{D- 0 V>j ;69< 8DH;87lDZbeGnTT@Y SR/OW%TΫ_2|478$T@pBMSwfZKݷM'=A=c!Uq/`,= ГGX gqFk|Ќ$f} 7CT 8@z=%hn3Bq3s}9ejnP<y ";^R|[]jj+.hVaNQ7keL5ma >A|Y*A>$b{>\gzI 5p*8Zpk" W*j266~!PqeN=q_/>ul뺱DTj.}Jwo(a؃w | ڷ [?ߛ>Ԑ>y 3!'??l$NYS҇S>[zП W@5 oǪ7VCUosǽUofu (L4-4 l0(^| cǝdJ#:(-I, Ld5+rN18|Yk"|ϑQL?@?B==03y#_`gk Hpɮ|k{ケfy]igH.s^u7R.4u}c ,3!JNyvRYWLp?23f/lCYH<攙aJ8ARAع[@gw -\Pyyt]s,0 'ι<^GHь{:ԪI~Ggxnu7?L ^;w=<Б$Ju]$~4C8BK޽;':A"0q:˹`GꁐGAԊ j"$I'G!zz#> o1ab 0ZbJhy?+VZ#Tz7= `͇P:EA!CD Cs}sTpkE45 5p&SN vI~@qv^8o~Z{pjzQo~"vx+i^a& A# s~rEODN $\c2cu&tԇ?Y>~3g4io*zsl.ݡOoN{$}vo?lMbҗR[wdՅl=՛t+@DA zAY~Xްncqۮ}~޴'(;s1*8;kmw4!W>ub?8[uokp2$Ղ=+=jeqzԣyM}>~(s"މ :gA$G؃PW{jo|"J1ܡTB+HP$ոK}<̡v%R+ })\zm0Hu R4Ӹ~ќ>Ї\@|c.^AK? p{.(QD|S8~pn 8EfQpӻ~!ukd=u׏r;{_jP#_Y~h_ۅw\7 OBO?ZP'qw-ilC7 nQ;q}@??MX:g@=y(]>Gl8{ⓞ5{~6&Mh ~#4=fMyoq˴nMKZ{?Xeg?{f XAVOg o|B{"& cw0bz uH#? #UC/!. j?8{@C[L14:|pDj?6pN _V%dx*8a\?u1=߄Dخ{s٣Y@Ycp9GQ!&7%XbHs#bGA 5A`cp4n=^ ds}ow)ߖ<b TW ꨇ$ 8*[}N92@#|p(Ǥ}BBn97J hG3ʩyfr\VW=t7-nk#||ἥckWi[a*}j/%X-7PBYB!?*HA9׌9(" |$sXOk߈$D5PiܤI&MhN@lhM ۮyyM cŰbhV8~n\qD $"w @!iUmtq?}խo]u뻞-XU>)Aq=d;9<u'gԀ 342YK}@O$c3ˇ}> |0Ϝ[@3oʇ9}ƬSHQs\@0hD]W }zwXOdrc5Gc~c Ab"Oz5v6 f:=}ݛ9R Qty@jN $ȣdح (*!AEUO] 7~&ͣH}rCNCAѳ؀8}S"\QR!>QΆ$=ޘg{ {q;};G2N(s`bu@>kaP?.` O^Fs!@ is ;~üN@82 Pp O@^?I&MhN9tfh蓡}BϘ~zw @76&y7՚-f? ~D7X;@eݮ~S|Ỿw+_MK >?푻V'< pqG5C􈀞oU ).YH5.WV@K=SLNX7 o8$p`Ry:ȥP{ŌJrO;޶bOT(4Ϝ~Wru-# ȣ[@9G~У 5cdD.e78C͟ڷu?5VUVUxr@T?-wGyF>~,Wֿ5w4,jA: Dp< m s;@z8pYk|^6͹|s=9!gX!}@7=?A"J !$ I@@=+Qޭsx,#*8Dخ~74iҤ Mi/lzZhӛ>g_x$Zغȇl=p?gO$P >9 sq'|,=ڡp]7yV?Y ,TG_99@')wy'f9A-g<+z72戕n>TTf:bfN ; EԺjt5[ Q暅=~Ff5q5G5n>6dp=>j{8$r~?`ԉ>yD G=;6 {wo b&W=`Q=vhPhAM ~s`Gw2'"*=\>ѓƃ @9\+ގ{8}m R~ \q/CqFv߰ه/MӸ~Ϩz0@Ԯ;pU+Akk< 7oNqO ~3S"!ԭz>P(594iҤ Mi*Bæm~mGhƝ]w?X$IEo~(jNՋP H7U yt7?z٘uoC:|@G# #>~Hwu=` OXg+ߔKs㎠ q'̑كpě~F8oCdpjLG3`L ?gPԡBj+H8 1"(Ӹ~ $\@ uN{[Bȇ@Je_= ۧ_֬xSh= G]g?3裹@wdZ]@!Su 0hR?!2bIUOjf{~ u܃sz![&wj~Ψ*!|qD9$w V©nHhҤI&4} Zzvh_;Ў7}1B;ozm>1[s5?[qz0?ugne ~8~ bm =KkƭYܣ^U/| ~Xֹ>ȜA;M- vGnz;DuInxgC uBS{Z g'k@;?! { {@)o Uw :y@~x߀@SzЃۧ#G@3I3)k?Y# $!{?Q}vB$A*Q>`dNC"ω|k@ND@[@:Y+F [Ƭ:~oDAԨFj~CuPȔo{A {@;@MCP8PFՀ>*wk޲o.fnŭ; ~>ckqC>- T{5B>CGoRQw ~4. ɀr>:h5rT]?qq=fWMhҤI&4}%:;gBݡ7}1BۯgϾUwqi!7v˰gáCA0(3JS뗼9s',}l,=ÿz[!V$؃l\Cv=*=Q(s'v YF~u2<:o Gy*ṙnITSA=- OH 'rso׽r5Jz%}@Pu7kAhnG^ =ڻ?@ !˫+A!@.C"?@<#lA@ };~Gj>73,@ϸUp<- o=H|V>Zrz@)?pN;Uoj`O رpU k߼Q=shZ7kު9t7~ܥ7.>{yzjdp~HX:vN5'j$9r}O>?9A|[Q Pwjֽ)*60?WnM4iAW~>4e4}9B}FN #أx(nuX7V m=9zE7gI;}p$Q #׏+rѤ>o;Sڷo>|wqcA xyrFn0'ԖKԾ.xA:(9 H3@Gr@xK-z6CG.!~$bu `u}jD{zl @1LB[(8~Ms(r 'T@z#Q(^'  vOMЋXW! fc8D| Vß>"nhR׽z82W,PHPi\@buOWW}@PqГ@^֫ȕ u4|dٴ:@R?{b YN=ĺEC]@("`U'sqTBQlzYF^@&M4;A7sh6RfMiЖ~|vփ$9E5' O:~m?Q H7B<׿.!P@8@DŽ@?"j!qQn 9x+\>g8οYxU}Dw~V%P (5k=$ͣ$;@HrO3A뇼:|'"b@DU+Z䩱g@&M4雾U@}44υ^4K?0;fgn3ASD zA;nJb[Ÿ=:rGhw<`ّ}n~s>_!9qC|3a`˧?9enI=WH=@DYBuo7S.>TyY oY +v~,v;|A 㧺<I nĘ ;5pq D㧊wuHəU[d naPknMԭ+>S:e2R^{PMJ|B=)jV+wOq >)=ސ(p讟|㎟nC߁ >uȻpO! t.sz!{/n#~bck|Aɨܡ'#~B'k i\pCC{xZQޏ"xD8},(/P p {%޺H|~hҤI&4}7^>}uOoφf_jg\}/;?s9GéA[B5)jVGOyW@/{s?eo^ܺF+خH8|j@-!A-\@F^@HnլSУo (yy= ~ZOy }! `PCmTuelupXΊ7#ΏxmN1ρE!jK7!Wp^OԖg"sq;@Ĝ>C+pDw)<Q 191n^az7.Eri([7yd7R!?B'g;"9N i4.%Dn=$1' tg?jq;G-c?y 4fKOuU \>d}ճuoPh#gTzM5+C ĺ7fuݛ;CmN` \D3أ~q @ z4iҤ M5 /f#M?r@C{KۮylÎ'e|xnJ% yH9$Pyo>Y}{d4/ɺYF;̋n\5k?5n`9= g "U9|Hֻ)7ЯGn-V=ZPAQ8|zG's`ќZQ}?P? ި+ߔ_]AxcGr7 in~X׿&k?!@ ܤ 8sZ$iD!@P*z@I3P7(2GsCG9}V'9n x: ψHTj>+@n'wׂ,؃7N >un=A0Y.Xy>>꿟ms}`buPzDH9}Aˉ "rNӇ (kupg _ rr^OԶ WD=V&M4i@7}hu[CQz_7}~ϸk뎇ͽ-Wev֖\ka ~w|\1s989z랅+'A7z{nн u '(j{C484!'3s?C_OD>C=zG79?M AC'3VQ+؉$ rT?[Yށs=Z1rn&AnQs n >׬Ygլ}S i&OJ_~x>+3f &!W-!z֏"}z}(xOȄH3A@zvup} ^'c ~ȣ3X-Q7Gug@u05d==j~+{)ub>Pzlr`uJkᨙY]A@s!I5. BxglУqSoxaP/@P'x4Enwx !psM@;iҤIA }f#KB5G~póz{@m;g/y̍'rb@y~S;4=^7GYu7G}fl3]Azr.QN4Ӻ|AOqt2$J?I3ֻ%oo(zD z\RӇ(w~9gAp3,;T8DL @OJ3zxBC{s4+b 1N pgR$?{NJ7Ez>\R#[ApS 6U5pOt3 {?`rHJ'z'0Tn&ou%Oj78}b&z3H 7z=YYfç=NQ'E+"K5W )oDw:'2'bHk2/^~kx" <#GR xS9Rvm4ߨwF[#puz'g k* 7P DDm+{ 9} P ΁!Ns iP |fԹ~*wC@7=QG{֝s]zo>p}~Axb  "W]AT])8\ p4'*rw~ 9@9Bs&P>e_n D{ N j3vzK˽;cʗr"=|A}5gE E1jIxdD?pQy/b똇RQgM||/7}7}w i{(?~rOОkHϹ9Y hm~t0Dw~ C/JQ .rV D%I؃(jYhaxGҹ{'{?Y Mê7E]"TPwG59 hG=d0Q@Dzr[? R- sGpmݛj B (v #AC!1dP;MuGifp n(~9?<~uuУhu'zx qxw~`= Љ tudoUp=6SP;@q8d/d}D(؃GO?287N8D4tT;!րfn!Y?(a!B3? x< PZ;!MMhM顿 F L3zlhwDL-? (w Ҝ }p7|eN7'yq>!˷Ewp   27ք6{۲cs'Vߓ@GǏQD\K=C^R9d #= $Q;qzȜB=a \ofʼ]& PDa \ 3$zo zʉ3C4C/D@@_VkPD>vhGn 9=+P==RP:ShX=.teTM.yo~` z d},4r>8Zh!\iMM -Of#Сӛ{V>) W;$@f;v<߾l46-9AVxK*sA\Oۯr>yxs_{V#7LN9rQNTミ߼OC!w{OA|\ɉ+i?ǁoQ}^ v(@ )m O?m_Q*أxRAM}\?!Ez!~@~ zWސ 8190u swSpA; ֽ'ޤjyDY`P{H`rTab? ~?M63ˁpĄ7~R*8]&!׿~H $W_9tx䝟U>z>D 9nCfȝ>n9#'}uu9 Zz@8>:CI;B QT:CA @)?D""~;`u .z4CԮZ雾雾;E@šwf#o Mwp<ϊxcE t?3cmצKdv8i h}ޭY~2 %yJ\/0$(bֺ v+e.b@w~.~' ~$@H5\>LGP?~92)9}o ȃ D=H3sGz㰇[?qS? =< h9n |G9|CP}>WNyJ9 ?nҚ7B!|V5{C}Q_g;>ؓy R$#kjހP|ab0Cj [vȿw[m8O({ʋ Hu0:c.ࣹ"g]A7S1+ހ?.A? rȃ5. $"J&ٓQc'r`O~ۧsUԁbfu MI9W A17=s 7Dw!s@?雾雾S}MwBOnз&H[zOjN>=4/y~.x sh(X t@p,{ۺ#W=G$IDsG&7 ~!?P},9:s~:$A* ~5p?=A}qM y$&Rߢ9oHo7ո}6 ~DE˙yZ1AM~{@rCo̐ozٍF! Uo)_xNoq|*!3i׿ifQ =$@$Г{'^Hn~`T׽u>~  upzGs) ּ)~=}]}RӸ$q}io(VX/3Adpg!ңQ} g H5h9- ww7r׏DOr.jA=B@:~CT=A8~|@D4ʷTW+|Tg^ N @qqSoO d," 19GS\ȥthooN6FBۦYhW}l{@q7is8-Ø? }B 9P #VeR~8=Gj^>R@p)ja}wk2JW Q=xz 92d%Q$nnX!f~J@O rx+orgHQWO]:"!C=p H |2: G=\>Vɘo*C!;~'s[7}ƁbY&gORp)okܲWO PDЃ|B ?~C0QӺ\P j>~L[9k1?E|9*+8b*i{r59N #?DnAs;x3=ywH.Gc2O+wtըJ#@*ڜ?v$DcW NgS}ݘozȥp W<@ Ղ@= $Уѯc0ǀ̣~ 9290Ú7 |z{T@;)`Dd 2#Xw9A@!}\wʊsw#=Ls?!?!"GQGͽʞC ;1tWVN A[  ?8y@@V?vTK9nKw^3 =+w?{>PB$AVQ8PwN AP}\oT75J}??AM6| ,Gpt9 @'EDOЧBDdYwX?grOG أd+ؓΨځ~ԣ6571zC̾М>MM" C7Oosn~FI7~OrvΌ;//4ݪuzD`6UǏ4@"K85n @D?{Ǐ"*[9~B@ɹr$ew7C-0ļ urOi^ț;@|JIi\b2Cn+IH>ZsseϡZTC3bYqǝ@os 5b?:1i9rV!q; z3|C$25ZP?Oʧp7}7}wo l>z~ =ހyu=׽OO7l;z~r>Ё ResE,D1 |ܒt]quo vQ5U!+<+*GCPv H #@O i 9^?8~ nf\sءظ~G4Go|[+ f;J;XF]ɘ2 Zr@xr@> g~F\QLzHw;̀~T7ȿ&!$KN;CQ5ȁ?z \0B w4'@5GwC?@=;@.S׿'Э8|DCE~͛)ec}w9?-5 3 .q>,!l8)@B wtJQG{7H913cCDWqUpY+V׏rN>?OG3@YgwgGpC(DTn~Bz@=$0萧%K)Od.gk?雾雾Sͅ~(}6wzyTĝV|X@@s?9??ζ_3'>fpL`ky: "P*f@ e~PoO:o_5u +,(8tΟQ+5CaQm?=䉜w%~ hgU?HPp[D`[G=97R?89M jOkbJw%CꔈCeP;3zIPւ<R x$p1WO 'r)+HYzoq(Cpo7~jG9do UZ. wUC;?5p!I4w!G83R qH d(z.P57tDб~?@X@^kps޼8rx@;?z׮8})w؃p(73%vp(zcsd=$)G|d Y/]>"@ Tyu0SeCP]@R䨇<}~Ϭ!Ј9R]@| 81ϝ@P"~?}ȭ ImO.uu87}7}w*;Bwf#&giK(`OΊp<;㉮!xqΓxf8&O~^ 'r'jOh})qnm񋻿}jޏhz^@~{V@g͝?.Pz;D(j Tz^,(!C0zm rRLy(Ѓ~@9(2 up P,AqnQ?jiV1j u>9qp[Dt ~@$nQ$ dRߝ?zu~PYH`D 2"' C&Gw~,]@ QO5} w By&ourzn j?+WQr. Uww.7\l[H=IkmX i8!" ֽѧA8|,yC/- ?zd\!DCm#D@Ph78d'cB~B roF,Oߒq yh )2@ɜzRnX/11{[Օo|%к'4}7}7}zHWCN? 5#ԉz0Stev<8oōK???3"/n5m~)2g^e:r\Vtg.$O .ß{vbgkV_sLkh;~+ D2p(NsG=ktOD@У*q>cQ`LΟ')!'\Р@}%ƻ= (@dNVHCTj0ozD@7=Z9+Y }?){kNAu[֭Wr92}@-Rt#O="> (D~DMNU{ u}D7f)rC} 1zjs7w~j;WpU+'"@g%:r _xj!wA75=@I@{##Q;k 3@Cq g!A|<X4 9^ ;~PsrI.#R XO4-雾雾; ГC{CL?S<!E OS*<'dn 0Γ{芷ڲsƲJЃ@S =Vݔ_e\rVmo>@7΢HHH3j 3RF_׿ ^uop* brf{L:tӹG>U#W=e izO_Wjs r?B*C{@OF kހ= @áP؃@C.@k=І>޲א^G"=1 1OF>飜C z#@v7RUp1׏f=?̚[?:_'u= Cu\+QMT]?x!>+ps/w#||]X $՝Ç(qAO~I@r`O(rA)f :e(HO;S rV8Duw(ޘZ G3wTOh>~\oRYaýAiAǣS ͲͶ?gOZm9ym~^-s,?g^ȗ+Sr#0(%0(b8~L o=x?sv~~p}X nYq珔(Ċ7j}BZf |#o^Fu`>a3s}~j_39>$@Op+;@q=tz$kߔ3>J5 zP* "Up#( O~BA({wq2y@p(.DKc}!qFPq)DQrC? r}$=RoC{9BcVyQwA@]o!\>=fq]7b"7^H}t?}$O 4CW 9QOk㺯;Uۗ3rgw,sg P ~׃ }Aq@( 0 hP};wl[H5b> ֿ!HG9`@ON 5LN f8~C7R=U#+AYopC5'bVG1@g ]guu7| N;@@RO@H1Ap7}7}w*C?_Bė1$@s"&ԉ?%zq dhP;m-{>yBk6musA~fv/sy& C Zh. #}"_r[̜yCmM Z5("7jz=Y?]{׻m>C~G=jr@Pb أ Hj!~ucY'c}Q q(T Tcr^>!=5H.a͛q׽Y/afGp{8PAz8 qj78?y"&}G̾@S@sD뇜:NYV̩wԓGP}2-k4\=?SnG0g U׏ב͜@$q@*(%Q?DVwM֊r zB@ uoY H̢[]@WzCZׇ(֝7>^>|7A>#$OY(Tꚻ?K'%Q. hf7VTj#1@GsmPwOBUpx= G=@5pz dDm HzѯR]@r;3h0HMMMߩC#ܗz ?KSkOjp#׽Oo!Y[̶\n?5۰ȗɝٙ;nu}&zMp(c:pVuh.־-ll\|n;gl":oP2ê!@!D_JOsл,5- 7>zG!kT(D]?oSF@uZr5YB+g Hf9WeB9=?J p!(T{| OLe?VWNV!A:0c͛ OF~jsEMȁ=Ϭ[o(&'" W n fr{p$)gH'rjC}2ojA""'k%?![wH^ XQZY?ʟsqgOQg]V} zW?ʻ;?3XڷZ#w fC-cHPwxIE4[#}rE!ypTVetz9anfZy yD;{I=\Ú7r>_S׹ѣ5y=-sUzgh hNi-MM"hM虡 Fm}̇J3aF^jWwg]_@mzIAmM39pW Q^)(@rݖ\#o/-,{;gֽmZ[|n62nGF)zA"'z7xSC_"uS ΀ۇ~uçȳn>ȁw~"Rm>Dn!gN QCnu |3 y i#<Q_"C@Ԋ 쑺Z?0TA=OuuԧVر O\({Đ1z: 03- ND O?y/7J'\A!"GQ2Г=o>us R.O],TnS 쐣tz৾3W2FPꁐ+H=q ];7QgΏ,\gtuɩ?lwU#pGyʡi J7^ 7ɋ4$SÑ EdA8 7`'92#`O>&*?5p WM $/HOM9@ 飚vGP OMMMߩ6^th6B -L?YzH> xR1o)?~Q}Љjo WKs^O}I} 9Wκgg=QΟ8^@\RrC@ n$͇gݶCwIrT> p'#3"p8D݈?Rm~S]Au}[+ 5v}$qEOp'b U|˺7u,䐧+X~7kq~[J.#qeP|=T؃61G\sSB?BCSEdjzJ8 [5p5pu+p G}riNqGNV@{?GB D׏Է!= D\`Y9}+ qHyFOsOʑ͙iA53Z8u|mKf9gg[zyIQ@1 wأ<4\@#BTЩ.Q9 { [bћ̫ z yA"N9b:0e:.sV![h 7sVg^{;?rT?Q#- wUG(ou7n9W\޴nooN{CГB_ C@f A Dl Ў>qHM!}g;Z ^2UJs(?҅!neLsKsBYܴ X.p,\J*8Vm>VpÁ@B@b?y;s j9" ~ 0o$xҪ7=-'doulH(RC ֏|@Q{h G}7@ weTޭwS{Y S3kVn w:||G;rأsdj>WuwL[-pޮ(mj%ܑBg?{ ;׿1r[&8䠧TeO~]HuFf. : \ 9[?[@A= uЧfGp¡ =}EJyB#'<Ճ9;@7&s0A;=HS7雾雾S=6CЃ? V\'Ur 8dS]?B/x Åm'>P{O9<{l dn(a\@PŸE9NDZe{ٟETP~̳j}RN`կkRjiRpg S_b ~N 9є}_8Dd6^82pu ?rEVI{A@:#.țC PJJOoʇoeO'PU{Q3#cuhNmsi:p'$Qw"җ;9ܡ/9 b. ;`P규wGΟ r4ߘ5rL 59Gdlo*G=> qԯ`PΤ+. (YDG=jYʙEn O`\1i\?1 (D.gs (~2"׬uW @Wz8DrA:}\?@;ͩ*hthooN.ЇC|O䀟_qoErG` ׏=RCmWsa  Xyp";?f, 9.=#nE,+n ޖNVoٗф=@y'?? ~f'1>rk0pgHuB %J`pzcpH1j7n)sNνohG=r(wd@xE n 僮T>?(PwH:!g[uYVk QDA>x ܉9,K. j@=9P:~rNmJ8)ݺ?rG >C/!zĐzkIis$Q{@qk#ψ AROPGR=ʭ־ }-W_7ӑw~z;z5p?#ͪVѻG7o}, YwP9$'kb3 O} @HPBDz0 bFbw~X!=rd?zj_(86C B7s Au XQ~zԁ >~˹rfzT#J{su87}7}w*MW>GC/~z& Z>y䵢rn {|p7|㉀|͞xH8~O6ʶ >t2^o (sVIr(bJw e/ֽE@5$qSFG Ns?'=/wqؽS_P!AtHP #;Q@(E_?+G1hj@scouk|ЧFڠzA>(Q}1W-@3x=S9  bw~ԓ5pDjri@P/7@S:AYӧqC8'!@0(t2uD۬Yk-ykaP $ >ǡ?zJ8OO!{D@ou;U{ƸOzZ>@}▝5.O;r`aݛΟ+zH0 uT#>04P! q)#QGr +nhf.; fȁPYClwݨ?~\zSss#;?!V=`N}P}D}W˧C[@{Dr{$u˚MMM)'#L>'@i0hޏDow{$ٸbٸD D͗h?9;9~4.? J4HG7 #5帀ο5V;uog s4%O*`O(?k|ۯլ[V+F ("zA!){& u}+q$ TA9Y=h VqH` :PB$MrE ,MD):!Z@ޙ[M-5S@}[>w?i>{d. CPDBD=ZnDwyG:vV8>;dO+uo dN^A7 JE$ 6@Ԃ _~wYհckwXW95y.#sXGl`O;sOŔfE9@ \>{Sx#|A}~!97d'k-Iv6w΁?^!z7;RYWDAw}boCzpCq B8hY[VU DJH4oo@CQh6Rb}_@3<>bRT{q(QG|/^k7;0ذH@:F/}윋^5߹/@E;.ٴwvV^\ƀ?oH('>X׿%b\(;¶C xH7KZ3j\A![G~[oC+5sVU$ȣ9J%ԁ ;|꼻j?|jn=s4 QNl]A?l-\a9o 8CGQb[zz@=y&@s{P'{Ը~BѝAk?co)XfC@(wRuhcV)G fZHՂB"5=5J>XW©Wo?HV55*FM90/~G߀pܑ*@Ha}rE #Ak޲T^|,"5O;pnO 8[G{BysK{S{= (+zszC׾ t =M *zG5rI8S?~ :s־ip'`rN;G9 ?D"?&~ MMMߩBB34φn =UoG @O>M{@-;O$gM' )•Ҁ2?snَmfmYzleomf|k?>J]ٗ8GnDȣ;~2vWWʷp=(Qȅz#ͩ;'"yXy>Ԋ~=\@Y >Aw0;@]QnHt0 V՛?́@'qz)?DPz洐?2W㇘8V`~[;DzԻ~9\\@ \~@r!9#g&|4&D@jrj|>G!jֽ1 |֏z_Ƹ;?4J|" d ?@C3yCBN8#:MC![ `'sF0${S=9W/u95gC^N I Ի?jS~R}n`'sE@8}Xf9 ~[TYg7ET]?R MMM)hB7N?/ <>!@ 5zǻC9}sU}@k~'t49IeNة @?g?=ζ<ݳ-׼kw6= ~6:ʷ$9'ٗL^0H+ߖc!?DI ($ r;E }ArA7 ig5xU~{Vzk|$ ]!=2TepH9D GmGȑ:P'@\?04>a'{+5'>2PA>|>(胬>MzQ@䨬3?Q-C>k">zN vq!?>(#(9h7wpt'd Ѓ5&vz4 B܄#?@>8r?~S.E-؃ VQ Rn <}n@}i @ >o6ADM*N az[/UzZ ??8p)>Ш5pyȋAz'AD?o/cԱbᏯ| o CL"՛@DO8}=QD!Ykw(ɹj9 Z0$|Hy8CdS@3 Cv-% >CnQoouoZ_>xȁ@= >Ya՛D#zsG2SAnjԭ}S4( N֊8 (:aGsvC G:`UAH| `;>PZC#.sz7PTW7ϰ h>mK4꿃 zÚ'yC.'72 y2'3^F:jb r@w2N5R-pԹ~ wԻ@O+@=:.huz@_fHOb].dGDORk z7(wOA:~ȳdG7OC Ѓ, (Uo]3oo_f#^||B2wOJ5=@ysBkֻU՛@UM)r@PBs<@^p ݹ- P@Z\'GH 'ϫrۙ1 D͟?nt.I5p@=q CA !rge.ЧG'\?8' O伡_w8V~2Ϛ\}s *Ec T#?B}Sr@ H@qw\??^Q<$ûG=eM^Wiq]Sa=T= 8|[kȣր:/tR huʝ@PH!z$3z/ J=;@8 y*swi d#f 0 #k['rH؉׼"tZ?8y7{rApukBzAi ?zQj]? O.C Rl`}@o@MM Б'BdЙN/><"䮟C~Qx r!O]D]]@ޅi UĽ`IЖ+|vVA\xlG8u.;s \hE,ޞ:G'V@rp[VBt"Zdw'|'z)tdGG)jOfs~ړOv ?rp=GT͝>㧃APcqC2u "Z Yg- "v}~A;=|hXg *5pGb[rF A2GCL%q7y=@!'= E: Zo@~Rj{3IPG7;&u/C.;վ d9*T7;`2sfnzAe5 m\1 1Q&8*)nz7^hUٗfkYL2JR5$L2N9h$NEy?O]fzU=n\Z>/=̪5pq[ O;W_o[2OiTQ[1 )}gyG5Ӫ |=QZn֏!f丅C QOT <Ώr{?qR$CT-Q̜Ι`Rtyԓ@Ā85pH\?!E]@쑘|R!kֻiFF*\?N@O%WB =@d!JC}du7>f(z]@~7:6thzD}/ta91gmO'fb[ SP*ޖ&D r'P! }Q?G\>!qA@l+ j=-/nQ>" A n#N[@K裨I D$W,?u4@ ?EڷG4H7)s)Zcb[ \FgrPH5oz#[V!~|<׽DPKo4V;(j?m]p{=8z""ViV8PqcN fQM[ {?. jQo#;@s޴SVq/NԷ~y?:w~V䝟p$7}ݟ8~ 귬M.`(ꟶ;ж=sŸ\@w?s腬gNΞ7@O Dޱܜ=@  *b ֏![?)oYR\\s=Ԧ፟fw9SKW<weh1+ g+޸T*}}jP:/Å[ǣJ@ԺT u =@H} 2S;A|k߈ykaNjG'd7 )Er pA)k\?}GB>W8>(]?7C>c iv R'b Һ7:F A8A}*1@^w9?BDB@z3 D5@ (o  _&A@@Q yAq'59*nH@EO c8jP |*S8r jC]~?U-d<bq=['yČpZ8ze@!@H3>7B{"JM52 DOrOΨ@5^C!V H|tȉ | dN#?vٙƩ8:ڭr[/^P@E?yjCOtJ c^֑Xdc?gOHVnkckP}9 _ T ճ87B T2\}͔#㩟.<}: ɨ:2n, r+gxhW@ώ (mQ(@( B N.<@['PBwO*5#ŝm:Ts|$ۤ+7V6Y"9赶mTM_Wwڦoz{x W5ϻtyR-| Ov5P\Pak߈R@O-C"j > }Þ:w)jP!zqk얏~B!<ܡG||d z9n:R>ǣz BsYt(dpI}CݟLFE?GΠ\8,g?YrD^G/ }}\@\T C.M>2G5j}7@M/ ٸsuS`;?ߜ{ ϩ^$[F-n8aݛ ۈZHG zŪ7~ ~TCX3wԬ~v }nXn@8~M26+$\?C S.\>|c`pVe!1XQMy:},SsSH}>& 0jṔA07%howchzDAu[q>@X)r>.q%ؑԹ 1k{{w=z(yY/̧~: =V[1 NJ’:cm^tW==phM}r܍(UnHn Ο'ʌO]:fsSJ4i[t 5pyA'٨(7f&,~5?{ܫo ݡnkjj!7CO5nU}Hbi9'z z6.z QCQ Tv4pۇ|tw?%d-CQW!nq 'n1Ō5or8j* X^P{($CWtG QG2c9 哵9\PG=;CH Aڷ!oX󉩅HgMrħ7~2.b?u͟2zB `G=|k9 PkrP{Nh7́;Z~R1'WMyFH2S*qt;=Ęu )?-IQT]A!E@Dy y@P9 ӱĝAUp7}VPk'9{GA> d'sCbJ'"'!7;!C?M_n-u?ؼ-Z`O<Ln kހ@q(rPg>u x޳3O= Ke_y}k,{G5k|FP%+G8k\G7@Yrgz*b9'?>h@J?|2p9t%332]$R' @C; 3]W?'5uŽ oܜ笭wꟽ!_G@̀<Gʾr>.а&}%O I3VtCDֽ'rf9+|frgqm sgf!"G=Bʁ>%@1"H`o}t:u}`j>9jO=ٛ.&>.ArŐ!؉YF ?@s1xj{?'9 S瀟N}?{#2C)Vet !oI̹1W f@Do!"Jv0:bV y D5ocś.txqrs BDH}`R?. 飜ݟ̑\@!= 9p)Vr @ho$ h_GЏf:ym@/1#j@Y[_,Yh@CCJŽ;w{޽AzDz8cޅ{Llrg,_{3*K^|4 [f e% #` Vſk;eO8i[\yhbeK3gs=.rhR| ~Yu%_͠@_K碉|[wrG [yhЏm>y:[Ɗ8>D)>ʨܜ?zFQ =^F@E^HP'7U'{|bFY{}yB ?t?{S AN'yQ.Y';@.P {؃?&@~y]r9[@ʷC ּEn} O]777UK@ d(sA!A"ɝ?.飜?} ({ )@<p8%rg"2J D5sQ3og9J $u%*V Af}xY/ީԒO>wq{ԫO? )@ +o! XB6ZS:}~ju; +~j>Yh4|9. n#r[(MԈ>;ɺQ 7zyHG5'P{@ Uw% ;S9zQ[@rPB 4y }pыh. j?J@o7":zQ!i9d~(oVЇG>+$w(6ZnxGR0F/3[rǿ;e2#@cwM/ pr@P9tf) -< f |5p HwgOpx^ҝC_}vO~Gu:?> ~t (@hz6Х4+g%k,O %I]p]&!P8wizB _u(rn+A{ ϙIRC>*QH=@R5G9 zCԝ>Įo&sNr rTZN G43c3`A s# zA۞K`C uFYj )?"[@9|*9|4G8p@ClA]2У\}sDUq;|}OHup83DKCݺ!rO;$s!wvCv! 9+"@z+NRH/kzq^Q J?!u}9{Xqouѻ*V)|r]l1owA^CoG'w`烡wG[`P68+  N[nGz49;;#wC/1q7FBKxo+T5_ ^yK@g}nf;Np\ 'P",σ@C saKF{;;!DLcw e f t!7Q3şOz/_>8~B^%&J \gzsƼl-[=8-2mkpГCT_w~4w5kTgT@ Uo} ԮBH5=`O hV9j7~Ƚ @@hv\5>}CG_/gOs ŔN)08!r7"nyD~=ouF>L |=A~@6+o!cH0H}@Ҝ~pP@7 |Yd"N#8CCzyHWSsp dGӺ #ٽ崹ݩ˿;Y{s_j6 TTr! P tQ;RMqD4$397%J2;jA}zrqMk'9}$p)6+@r\@ l&P ;Rlog;ol hnhu觡|h{舿nꐜ> j?RR\=Z D"@{]]uί Ьw|"43cXw8ؓ|-€m^}73[gw^'WPht+@fޢ3'0;kkvPHցВ/OOtVo%I h nf H\mֲ>o'^ȷu+o}@OA2"'bI|Ƚ䊙CjTC ļrw<@c   f[$u @+yQ/2ŽGMuou `PVQx Thp7nI+Ǥ;?Q:V}H͟)zvabMUo ~Lқ $Q|xO;~q%HZ'n HNhf51qg f8;rY.}l~TQ[߀ h{J8n(GћK?:hſO'_/?#s];? RYĔ!uCB;}\?*8E>%Q@ȀOւ=uv5!Wo)@{`O}w{=\=^2+`Oq́=~ի\=f~SO/!!4ֿe?%gr dªYOǽŗ>?҄?8V@ @ sy;o s)bDioS{.(}`P9 H#PDB d@.f@t!O(k<<~)g79cɜ{ k+=(k?]k$@%7ӬE)8~p- Ir>̀CQvP;2C \>A"9|dWY繃 jV}G:sV5N !zZ=Og@ĻP!2 pc={273dxOeqP{E!?iW7B>h. V+2~s^2XA/&YxAj{@:jW@}ZT9 Rr?8~*Ժ^=ˉȝ?ŐO!Rۇh~#w,='azy_1qrO^?v}s{?@̫@*S8}xr<9ZW|2Gjz-#ό@ A7v7;AЛB:4=]謿*st'&@S@啳A*C]vͳn߬z:]`o;+yV˅ΡKa] F懋%n( X5V@y:Oa ?;_ $!.w~fm*cʜ̆~B&MΟ@_@r_ 7ݾ+ouR-my !I~;H=ֿ H͛uo+69Np)rgj D> k*pv}2~H(RXkXL\*`5"O裈A̕w8x7U '}"krV>XyS.z%O\o^Mo~ozZ$8T#Q}[G- w@ʁ>7T eN]iDQyYûG{@ |pc~:|+o|,ZsZ?~ZDq=>&.%}Ɂ8P ~$1c `Hpfޘ~sG}_by7 PuGsSr"Gw?@ /Qn}Er jj v!Ý7p4w8^F rֿz\ɨ{˧Q}W rj(AqdN=@o7@Q) xs^h=G@bnwq=o7|YodU쟻뫽vϟֽY7F3Qg+bF O-(8nq_O3#ΟNV߮1R~Xp{P~ZzsmqTPĀ>{UϿn<7B_E r7o?σ(R~[QDp ) ~}>E7R-հ: ; Wz@}` D$VeOHsA> !b=#;C{}Qn}E: I1C"wpQLin%ȉC׺{;ݟZs< _A>pH)  JE_ :8jӺ}:&"7~ϯZBX&Cdچ+ߔ.'05G? ׭H?1CHED8U+XF9= l>Di]@(?>%A\?"t#o@Ǐr"Gf;<:O2J73T"n;A!rs n>x0{2:Q^PH0uBQ gP@$-T.GB8ֻ&ճB7;Aм塟G m :* B7(B~(- \}"y }ݳtžSOeJ={b=܆b[nukL!7M/:zܓ+~]ϼ_>l}N15/O {?H+""eۦ'ɝftf-ˡ' bt'@K]pؑHԎBBouEoؾOA ~ `O  OK?!!Q E (D]@Ov 9G Ľu k9}`F*? r)N?D5 ~}Tw8}XV!@j@ ~sֽe#xCwBA@=VQ\ACݮ3׏:T I=@A)r5\M 1[nL0sb[3S9ZoqQ͟VrƵ G?h+W]6ͯOh I?k B-vuoQwȁBjj[?rfQ}OOK@yK=׎~J|+L7 z횷0_in珻|VbjGGp(sQ^@Kg{?m3Q OyB'˻@䓣@^S#'"\?ԥt#\>'yRAoz(4=$C.sBbpIJx9=E81YSki0n|pYW?5f^DoßqC*RO~oL‘q;gz=3छ>ʿ9;iI$]&oHь1tgڸ@ Z (?YpdHA`G/Nsfޅ7}O- bB㽐 B-1$C>:3C^Xf\So)y# JC'c7M{yTy]>vQb&S8ќ>ɹT(?"hF.珯xs3 G A E\]i+cǪKIH%,o8-YJJJ9R2)˴e-$;D I HB؈}P:b {v{ows Rp{o>P{NF C`L~P Sz׮/^.PfO {>t̓?ǂ@67%4sIP~ X@^"$) ,Y@.O]RϥvP^έ+p;@ui%ܙw1 ql@ D)8'Rpdzgl?@=+;>hlwPJ-SJu nN+*yv PPJ𜄄k j/7J;`v2i7/]@P/2S7|7$:i;kWvU7N<WjXq%R7…2]o`3`6hy0vJ-4B(]tZt*lsdk͎@!mFMV<6w`c3ٵQZ>FyZ َ\Hvɷ^/p%cG[]G)&>|O>}n}_2np\sܹ}w|oRܹ%{}E|_?yX׫AԮ "eڇS޿/{߹qREu}$Ҕmmk!_&6B- movOz1M? `ARJJ) I%y23-9më!.# ~m9aKiuWw:yC-m9IU9޷xz.um%H gἢCa{dĶqKi&ZwFڅ8ڶ`]=;3hOhy]8ϔ[ԃב 6|Jmy^gS:NZ,>3y] A?WKFWg%c]c:{~-y[)Oӽ̳wO/ςh۰]5?R٢I[9PM̃{pl?ζO Fp77C"tdEԡ L$$xq-8;8x917CoOV*X: XY0VbӸgx쫇R)^u,.ƅq0kp,uv!-LX2n\&H NY0i7P5sSHu她&qfRrd: 9og=['|6eӡ;'S,[\:^ll8=0|mqW8XM qvBFs A<\3gzqU.B,7l ) Ry?S"|8͹R *.%AFS0HۺߒˮӿS}&i_",ίu6ml-׿ E']sۘ}ҀQ YgU yh+^R/];.-ŴM2(É#jlA+Q `[ @CY;:/Ċ&v#"<@S e}{/\նp??3<^`ӹcMVBA'SUpհnq;.GHeuLKBX~9OTDs[eeM'6ΜӘ2Gg+۟.U(̅A dRK;抭a:[*Ϭ!ſ (@ \:D$Ȃo O2erm#ϼS듹3JyaMtzl;L SɁ@4} 0Xg)S B jo߿c4` 6؟0 : ;a,}[w3׍6IZE@+Qz)9F~M7S[ PRA7}%_.7}@+W>]~6־S'vlnX}t 6N>O!oƹX[FZ؎naq%Y=T=l_! gO R_ yk*.IӲfל ˽dI ղu`&%Gius"saj4k>[\4Nll*;F֕_,GD3trDcd)a"ˡGJ3)psaUeZN!L9 b0~' pkʳAErMKߙ TVEQOhHȾlmG}S(6>Ö2Ho@jWY7g-G<;zH'9zyR(H%!M>X\ezG+LG/=d򊑜FXy,GR eLgt$dӍ(53(y2+1Ѝx߆4^*PCfMPr-9(*utb[ii=qUN%K6sEo\o pd cOnGmL l ;C/h;f2M4R7NIs).ޙly:֥Q~"%[w*n{q-Ս'o>CD[7^0e?.GrlWT6LIg핕1M@f\JXv:cR\̉'~qDsu2~%(>uA[GI4 El?.V'&-k5SW| sSqXM ;8֐m (NZ8*k8 ^u4Qò sFJ 27v!AwIض7A6\uY]gK#KWK)ר\d>yv:1L[NHE֕4 =:uzRF%!CϵYJC^na ^"^/*8 iSt HsC)Jէ23=)%iSq%TSZN~r`1on+oFٲĠ>3[`.&tprm~#GyP=1F eXO-USyPb/ev$E`Z=;7 m r>t`(Z5Y6a ʱئnc qK~ HDψ.}(e`J-> վa6e*;Ge>gl?87@Ζ/عoR/7Ysrg"@[}P?"Fcwv8(\o=Н* ;0^ d_}]8sz;^%o;F6%O+83!S2H)M2c𜜫 iydpݥU`@ॷe]+͹igTs( 9d+qS;e1u+dKI5 n<-|~jۂ:~Kd; $L`,c%~+AmA+*vq(,] AL əlĂ~@H=v5t<ڎ^ViJПe`-/}&ߵ4?.wu+΋ ?! fs}J\T]\}{BkR1aݮ Az@0`)aFmX1(Mvc@E)fͣH.`XX*9yǑ`644u}cZFu^SӻW.kDcpfSpZ<8[:;5>l2 W/]?`]"f G]HwAJH"H!f ԅ&mAu+*ƀrdNIq}dvh E6*mEJǶt@-\6zejSme(p~ ߡq>߆G+( 544} OŬU^d(ɻmZcq䝜\^6N~>]\ܶYc!lW~rS7f"}G Z+ad81/HmssA8}r5)46[V+ς9~- u 6p40 -̡J<>W(9h𓸟!g` 6`%M5;c>b%X^8x \}6 hr"mZĝ!32*A, >>+\1 :d9Z~݁ Ǩ;unAj$޶$ޒ p,3<pm1^Q2_ervsݗ'~/J<:+s$UШ|"zc)cyeKj2HGrwݘRw@]'#J-67}9Fo,>SWN"0H|7&i;u\)98Dx~,%KZ[Y&,*#T 0sE-r߂H4ukZXkFb頓\a'@eCӜP.2v~zLΉ'B%ᣅQ"R/c\WdHg8Zhrb.749RqsV%Ab@.'K~\ |P([h;cܔ})i9 {&dٜҀ1K.h%D Os}Ѿsg0 ;b `&~cz<7K&:Swo8 q@Tԋ:t[I dAS62γTX1tg\YH俢P^ t&qy'7#_,f@Ʋ"+_] _+]~e1q\P[;\{kc"Anl@U&D`1qmsq\ƜTxdi"˞@xG6챵'!=u$ʔ@¸7z~ŏK\p7nV{j/O -汸o;n"A;J!Sr@B-e.9V_D\ŹX޲\Yڑtq $јy'**:,|ЯmG$qѼu}o4%v[.Z kS p1i5=#;@|ut{P&[{v4\o&m:FIcJeK`.@}(*{|>iJ9'΁cy7Yz '3uI2#| -d<Բň.<#G?!1 >4r7"=Ċl8қ}1do~6 az+A1y/v9X oպk7t,ǐ>;&>Z8`w.KUԶ .%1M$N[唈P`}l?G8ڹ3/2ړmFBrtXD~Ʉ98HFf@'vvztl}Е) :w|zEv<६SX\ޱ4N 虓8=}Kq6߆=7`1P@P2B<@̫Sڤn̥=פR0=sX?yN<7>Uvy[,(8V)%1RvӂFXeƋ~ܱ 2ww=}X@9{cYgӻ_vFz/>`Q@t^I'|p@"'Z1㣿I1SyZtP@0>+ x,o/=`f@'뢓~q%Yh@/A +{ hb  Kr6\>5,u@k=`3+M<}?Ϫ,^vR;՟Qd:OR܌gPuhO;FR1%V܈2$ w):{[,OiY.ߓa1Y ;ffԢ1$SAA<ލWYo/*&-Cyt=H 6` t~ xk0F26[ww$l]YhE@XpGXI@^im}ot FTo'O)v<2}444бK-5@ck?px:,b/Qt'<5&%OfשO#Dev*ʙ#tbymO5|Ĥ:mgIAPFOe~'eu'Z=yFtc-IrC{|X@_dd6u9pKhr &Q{;"90w Ty$g&Ә,Sl|z&վKZ̉U'crLODiμ!9zN;i)oy߲qL )0%tsL9Ac#$ V30[l5z~9qX ΈQԽDban!Kh|[ 5lw >N~X8ϔ(u lМa)8xe~_ugXv>s$a$τ[w )gu ^qle$犳8Lm筓ۿRr@I[%0[9.13I{ >+H&?m u+ciV?ߺdHGfYk.GVڌ;qVaml?0eOQHzߜpѡ;J chl?"{KC?,y^5( o0V/q\eF}f|#cb;< 9G]gG̙KAzA%q|wf}_wx ӦqФt+JS,]@gV'O=C'+n]bnEYu7E! ''L@Qeu3Q6N[ݗZMQ%1h}ǦP{}\:m zgӒF}ccٱo~ٷxFS( ##ƊwqEQ\wq<^$u]{Q=K/'N dcKI5΅q yl$p zr}2*,:1Tg>tŃRz89!@hƹgAA|[ہy']aBw` @N7Ѱ= J`J$?>A>QN4` 6!s)G?{{j@)>9h\~inl[Kh#튉B AU4em:FޅV_;̳M'7vp(Q2JjVMtr/Fǰ)g9EPtnzч6glKha422g\@=N(k/oeQk`0̬Hswq$y$(fpQϏPe F <6HdzM<}x#?xQE;ahL<a_q|L*lvjr I,| 0p3/-lo=|dAx0* Emɳb;؂B5h&fZ+ms=Kw$򄞝mbe9}>@h'Gr@`w];F2%/)VɥY/3Tc8UmО^0^m!cknaeěk473` 6~D_; *@ "WHV>^WN*D?& @)%ҲɈsIgb#`cSf,HiT.^~|K=Kg<H-<5@9X)O9֋r淡@d@Us!h+ O 9 ^ }MF5>Sؕ05Duu+:}e8M\4%y~uݞ] GoFd;,Z#m8q yxYmc`r9"iPhP @:ڒH@ހX$6w< za2o;[vC hTF=n|[˛M=Jlq@2Jb#0x-VQ}|PnuY,ȾXs8cDcyP^quX0,YaD4:=} O>T(@2+uG\υwQ4eZc]T%vt"V>!;eBE\-wc@!q֫]@#hǟQr o} C3Ҭbn\ϮG/W\^V]Xիh9:J;`(1NJ){sDcl~`ND*ٖP)޷ 6` v+s+q^5hJ4aH=jw cy˂\F;@L#9/LIJ-.xn=~LKjl@|oLvkMB c[|m`7.T&'%i:Y+h~/A8*63@^?zesc݋XS#4y/bܦnZ}#=oǼ3>a|m`䈜;?7PD&{#tH\7TKH.l٥$WC h;;8?EeeM2u86( ?ү*} {q 0GoB Q’Yl&& ɿnq,%IS%$d<оx4l5?#5YS0O^RLl\$mq-d\fg3?\%?!@@~%?t'>Ǟql?9\?dK1243ү43Oo ԼM]Cʑ*N@aP+Sx|Qs\52;+ܤBENlaγ}Zlqn0*:1 \vWݲTaFpr>+bI y3WFk :(d S} ߱ItI]1W>/p,ǏL-uY~^t%|?@T@W_?(bei%D:g$3r&KqsЬK#Uu@D N@=LD3S:n-0&#D⵷tlmE, al>? /w'6p{҂ 4 :9b}M\~)\r֙cmeT aot x)uG.}qf3s/W5jq@9>R|?^Ǻ_O?ƒ%} "P&J~dmɔfZ_P>yqO)C߈nVX f2ZY@-;4tv* Am#=+t9r:L`nb\$۲ e=!bmEǴɝ<`h>I'XDǏA£|@@ popx i^K\\svq@JǛ|l#3){2/c@@m Rmh'T>o{ q˧q;. *n7RG$B8vG?sMգ`2SNrt룓{> .v?4X٘?mε}tKӲ]*0(ev\rߘh\2R;ݤ"Y[ wg\ ΅3NoUH\~PgL-"")Q1sIyKT;6 MyqpjF`3k!#8q> D7O85{<z/ ډH^ d@?M"pl- \/,(S6D}ʄ09#Ӂ?n%[VZ|?w_6a^9&8A1sTcnVH*µ`l+=~[Ї}%bƬj2` q `?sG+I/rrDZ͘+bi{НejrB{-IA U&O?C,ϼe oƬFae/7Ǐl<^? 8YŸ+@leg]'oa{my,,!ys s7#;H@c=0_=~ڂRRWbSAWFq痧ok '$c}寧6rm'4degҞmUQR}2P+;kmKMuˁtN gc:}셛GN܎iCI@*H%9ԕ4i.lyx}Î5ZfE$XI,%a+d<-gN)]afI φ2g6p܄ORDBAttl:t2;7o/E3VfkXZiPSZ$3fUE1 4IiFbа#"yDuĖ,yl[p2 Msny3caA3ss:箒3\xx\ٱCL-y^bG7m\^Q2[pࣄyX9g;97Z$n,従a>A.u ꝿ>E0Ze8o)/}e9r$ 螒:ZVo}?^ $KZ{z,cn7ː\?~L.`N(f"̸uOAg$TǞ^9>_S4\|gˮ鼴[ hcv^o` 6` v FϏuоC M `uei@DJ)+N1uѐ D93l~0Kz>{W:MjX$d -OM|0S^['[SIc\|l '49΃>ONb{)[mm LN\qv @/r8tx_nfVFEɉ J/-;4٣v=rcL \c_-}?6 "]к[ c8B)HGg78Ju4@Emt.u gѼw 3&si`F.$5'7x8g3x8}*{aJ.>2F S˧8] Sfv&#>'0<ܧsט ## kybIizg=iVX f>٘/)a8V1L 﫞{5y~,W<? 6`'g'88_; ޸r:ii"8O{bLYy+{Is/}:R +Yzv?WmyIɍ%zP++;^ dW|>^\`+]|[_8J/&ĥ\wlz{s 䣯I=kޔml\^޾;<N67{bk#݄'H$vб8@]bɖm >Q#NV'N;β/vud1-,RH>_y;3߆ !#åsѝQh(6hF¡yl^Җcs?L@O1(ŁNͱ= Kg&oG١^2ʴ (,u:>y7e[ח}~w 6` 6y~r۳1j^à}@@z'Pnq ׭XѴSw-(`c"(ɠMcNkӜ;OGߖڱ[1-c$&.Oy> FK_jbj:j}oΝ|l/ rϽ{u+;YVRTAg*iMTc4$\țoM{#2ZGAQ/27<4[ߙ:sJ$R_&P a#_'oe,tG X}|??<^E |Ô ʥ2ym~A9Â$No?;[ԵN-:!GhC5-oĩӞHhbq2⠹">& ̱103WF%ŻDq椏mizc,yhReo/zRhֹ$/5 9gHR9R:"sdk,5xoؙ񐲏f^]ii3|gGyg};(pXa5<W62RQT*ȀhԮMDG} @Yɳ~=b ̸OvyM;tR xRLɒ-XX)JPV)P`ͼz]r3%#mX2Xgk6}z~ `e=y=V1(=C_F=[,2 l3v?gۥHx/ .B d$D5c~ihaP=-.4 nz%(Sugyנ N:׭MoyQ7uM* %Adt?\fyoSO?t2w^m'iuz0ߩ \!0U!VnK @J<0g';t\ry'Na#͗# zqqZF6.*;hm4.<֡@ff ɘRr|7$1'dB?[bNy=P%f} (i޹&M/xpW|0fbJΈ'1}r ^*1e x$Ag+y^`NR-c4/怐(g WOH{@țɸl8Zb# Wlj닡aLm^ ;TFT 9t| nA_O'`eea,dj۱^_g(v.zťni3` 6y~~N JH^REJ[d엒?즸-1C QI9hAbնg*vl܏(U0d[G'w/f3 du}t[ZTy3;!pY^:75w{.>ҭ]qԟHX@t9Oq0<];?4}#QDN}Kǟ{83݁tMo cw,_2 `UE!dVW(ΉDV~Ue:9n>LXəh=^`g7qi{j7/7<[{8:c*caUʴ>>:67EDIy ux`֑iKɁvx/7mb[ btpZNQ~Z*h>k34f_WxcM+D8b3\%j#3Y?)jw˲%eJ|l =EAd"ld|m@Q0H%yO`m~B~-=;M, 4 v[|` ?*aHdG" n'6YL9T.%~C9ŲX,B&ɟHe0cNہ7 N+CYA^AR?>gb8;HϿB<!+I-Ksc,-'xFC` Ӂ 6` v'?`)? `?&m[U5ԯ3|ܘ"f ͯ[6(^W#q@iVL F~/މE?ʁ'rp ;ju\@_\?؟& @+۱h:&N 3'U"1]\\W婨dNV;T@M3,3;;tdr0:5uS7w  yM a#+ %;L!EVs"gd)HPN+UXA֛d2@SɳZ+ʰ+iAWM6ep[1P'ul1 UNC#[3smQdqx{Y_c`@/sɭe^GA VJ+Tg駁)|Y I?L@y &K8>mN@z4oƐs2cDuSp4fG?g},Tݰ3PKNorx߱=gG~#4A3}g1ifXbcK)0ln滂o48AXGzױ4|l@& PC T>u4c޲isJ^@Vɋ X zFV0 #EP~kbЛ /높lqNRc#'] 7v t)'}qy{l<?`Ӡ}@ .סM%h[VN׎m^#tpHY'`elbmuv#TjCdti;܏s1KpEZ-_ [>Э}8f@btkӍSӼq+kct(4?em.J7Rdچ93|a *,Lf*H|nk Ewj2?wu~miݏ"6[s.N3#qF>[0پ80l_,'~$ _Hc taine ky1"%qge!*ԳMx6 :s"CmtG*}$f6YױH/z(O25'CtNf&C2]omc()ʂb<h>R.nsr{0NOI[KDߵ=Aw͠cC.K1@#<Ԍ\c@{=3̀i0| JEA^=amkk Xrq*F;vgq @*F&Ysb !0n+Hka Ұ,Q|i;HwtA+އL>4K3AtWkWo+|gZYjŮӁ3` 6y܉~9?>@+ &-dŁ *{q\uG5r";Zf+ 9)ڕ>}gJϝ5mtz]7g7EJ$iǧ݋) X7eԢT&%r<馲wJq:繹HH<suN?32Jͷ|d&ߤ`7u;MJbۧZd1M֖ܖ&dh(<(7pDɦeGG7\X6ÆIs 3|! a!dWbu%ȁ2/ a[&|d{ɵɳ&MIx 璓r" Œlju_=ea*Ȫɿ|ԝuOlM5os((`Nk+seƶ2  gy|LTL*fJ p X9,6xU$R?zmfY}PG\;輪}Na%rI+.4:x EJ8~p%b < 2@rcAaFO a;p҂9pvH`v@TYqrlY称2?oN@Y=\F cls2~~Y %1),!uSܸa4ĥW%WoS{!ɮlDыƟ|nR[5Tis6~ʧ] y7gyR1g Ed"evިCjF̿\}ٙ츿~6ow?b>xݗҩͽdHZD,Z\~>Ϲ2Z1P\=ӐL@1(;\A$R6bq}̶V;f! Y- [+gE|mAqh8K 4 #!{8o EO`ތ1oiD[h~XN/c< XykXqs4`/z2[F}.N[ay𧚶c*aEx~r jdxPA3pRr_v^cKm2pL){,wvXZ_)4l;u?SsB-mHNp!b 1t[7kr4,dfLaKZw;f^'(Ou}eX~cxӟJWU52kLQfa9O5P5Ofi9P:auz}ˀVtJݤgV?›ׯ7m69z9@˻>u8oc,'3Hpud5αj>^bE*o& $\"rJqGsMs=D,e /o}w,4cGE& 71 rA7&׷PiH D_vҩ33E"y(VEy7` &\V#h0O\(A2tX0KqVy2?pY6w`- [{S<=#I\Rfm>Gaf Ñٚ$oq8*;]'Omzycw, ox I 7 v^y&';KOʆ dɾw~!7%3ж cˎς;}8 =N#J'me\Ui[iW*E1`o$b-&:PNմ0?~FGyoY喺NwL(r{#$+蝉1n_8^$+>6ɿ{|,b1Ql{OPl}Tgԋ2 u~H ,?ȿy\4q[@ZQlt@@tɆI˾d^+*(DVaáX$H*ƪdRI^FNu|p>AZ0f^ZQMmR+ nˤ"̑ueYA[+]i%zkNl-gCY [RUޢ62Y%d%{5+y, =C'I=[Pȴ8vso^>ŸmmF'g`Y8y|. >l+-`Jxch:mg:d)(*)v_{7Ax{祎ƊUbI_-=e䤅 sDxv&~T>A!f%L~7< ?_񘹤}}6Ҡqd1F@d*(X"e‚%> km\:tGljr .džxk8|粫簰d/:ߺ^Y?{rle E? ߠ~l 0}b4?]w_Z:ר;}l7aD@}tߑCϼxO:3Fֿ:sɽ?v$^ؾ3_Ls Roy7*)y'P4XJUvTzeZCA>[<./Nn[ͻw}ɀS'&#Ժh/=߼VIɝ;~kzGsZןk7-sOn_y#Ă5 WA IWE>J>q`QR6M˥O#yNJFDG;\ݧlA<((N8%!s#NHxO(kc<=") s5(߁:cvSZBC@e78lGE LV20/2ƐDo͸ٙPɏc*ٚ3 $Y 2f}(\Oc.=K|_mcJ:fr^h;u-ӖM:.`wG>,u]WwK}Q'`(fxEro 6`Gg_8ej߶)&bB4?_\>knbeRI"/겧܂j.m; sz ihI·|ϔ gL~#mBљt]5-aQf&O4nyr.˚s++>νnn Iֹ~k͌В<#*WFn'6߳~;Rr~׍xfށ-~Wr>—P-!k@dRrX0sj]'L'qs~DϾw#Y\F4+N? "d]h8E1`Jh#B& Hbv|D9Xyq,A1 q 9à&gl|@sDrmV. =/Hr<_]+n&>*=I?nYR.N(qf'\4ח~7uOނkST2JJH]Ficz}w^^V*lt7uHy3J5<٦N7lœ伉\eop޿xB&^Թw䴵xG>4'B|px=hԓ G_zg*b+6;c~[׽<mQ&0r;I:=*R/, Ll;@2K9׀ du8'47.kBPO gM%9Ҽ .S,~ֵ@0Y= 0ԥ dwnfwXfJ!ȁqKbzUcA-HS?0Z Ed(P28PP~n7~Mp @hu/8_gv9]9 8oX#*uA7+A\}UGPPA e?d,CnAPLs{~(7!V Ŝy0H٘;5Pw׏ܘm' JqFu?)vtni15/j n ao]˔*]2n\i  RYnL̢ skƜ^17k)[i85ms3` 6y~އ~ Z@nAJ݂81r6q!)c8t2FdX@uǪxh!yDt7/-y JIHfi&XUfC@w玽3 }H]Jm+;ǏmuGu[_p2Gt&r=zlo<_tϼ=̏^ܶC RIL^Tc[' :}1 mLDlgě$QA\^K&~| .GY֋ϓȿP>(@yjط~{G.q~\c5?ձOkc3# +mxc&9̿]tI{(67ȃٱB.?|} 0ܝ:C[J/AUkqݮc,9\-{0߼[d/ 2><3 0}6Te-|.lb6k=߁G>0r|"%v:utGϐg# <8!1ﭖE$Rߦ>t cMA}/`Dє e-[ #\uD4'93ueBN&oDir|y^sڜHrmy$ CJ(m9gƠ-5%Yc‥Wt5#,,u4N,JLlORo @@({\_gǴ wa.R w?o1NN;r=9ȪG;zOZɌޡ(ɻ,_8[K`K`ip[@Y2~WI\:6qy?F <( oeGNNG7RQꐐQ(KK׭]3m\=Էl:y zX6O @:dBZ]s89VO2s²qVtLG>\:h 3my$"J"Z E~OX1Q>ׁbr)'Q"^ lInFml-bAL ֙;))俕jcyV-[5NlyՌpzHkhCrF@_AȜ3hUClЂ.?R,C$R;YU! xX@\ \p{D ܣ-ΉȲqa/ϧcq.<$_R]Yff||yq8iwK^"FcS;Iv^T3/(UP9ߓ?g%}~(?۞y=bŤ yF,ggaC6dy0Jl+crm8w>Ynro 6`g/i{z*? T?:[Nt ,ە9$5cbs]6j82tf2=|ꍘ1A4&e bU] Qe+Iga9lwޱ7˖O@J;HG7~['W_ا#Mo˽݇nLrɘEK7 Cf4o}$o^wˁmǑ<'pKbx m];>9sHOHTU鬑 98NukQg; | G5CE߁duK}jL rF}6*]$mkuULAp4I.ʟPqwxZg@b!h s;w~;t؞Kޱ's@c %uv!vEZ61;і%ZV E1S`(Ҭle8}i4 9??S$kALosTlI;fr]R#o+*Zq6a=U2*(XVk9 .Dkdm@D̖ɰe[f*<3+Xhau,X= FRQb,Q5jm"׌X2NL߈d!ާJ-Ts<ƾ 8~eQ&. ro 6`'-~k> GQF~&sdd@uFxSVq)8ZZ09 R$h^jk8`3梽/߶KU^Y@`{P@$ <rDP(ڀ(F\SXP3/> 8N,QGe:n5 xnb'N9>^< zHsUzh$; guVFC]`"x#vwN%:^qrZ i=Ŷ/b+CkfE \*V 0@#d?0ߢ]eRG)uedDALv,m>z+ T"6n>GHL/iڠ/4¹ϹrڹP͎"e4b;g x-s1x-ܳBOdpO6` vS?~$eH~G{ lA8InϺ_~^mGv(%D1 إ͔/;8؋H'&{Ve=>:cǓt.W>սg99 I O7gKT@c*Fz[(on{y{.{_n}mih}s}tGnA+&o9 L1,clԞ8#Q=-Ǜ61^lroh;?bGǖWGmyq|GEѫ-bt-i !"ͥ`1#Ky[2lPG-of[)aG @ m~\l  N٦1 %.1MnlWN"/I@>d|Y ETG1H李=vxiSMq= W Q{0, PƋ}+EsX{ot[V{ݯD7jI(Hq*J $˲َQbq*1B-Bd I'z|}P-Vw~YuI {Uw{眽>k_?AL5vb0t"&֟%$:@6??N|?1 yha鼉)sP1:tbOPmMKEdzCvsաW_*DJ >:fjo•&SA?N+]N fV[t:td$4Y8Onݺu;CO~E҅fZE^c%,`jɭMlΞۄ@(.xA D^A5͂bEm/iOxF ˑquwoj ^Lܶ'|6? NjE:&IWuƽmGv_OEi~\۶9\n;4uT"PvVz-ߥiY|[+p1ĽU5d/\zxױ&ad%Bb dqe//4HS@| EgZۓSE'Ǐ}Y[K3,\!^RA&#@%r.{ ӔwyjCU'={<ײ`& 3UMJ@zE 9@S II|AQL13L!>Ӟ,p~w!ajPfFґO׈/QcYb(8d 1K1hpPm8?ZRS2 ڌm9/sxȓx}bmK Z3R޺u֭y:?3ڮ\Agte%ԉ4 1:tK-s DY=,08s(x2UAϝd[ .ǹx\Fa'#d\W uS^pGpddgdbPжgz$_"O\|*rƸ_GAɨ?e$i>[#FP^6jmcqeD{&k.xUM{NZR'p~Xfoa@J8C8C)ϻqG@:M=<15r 4o?uN *~4Ns!6 WV`BҰf+ hj30HOo%G~ b]ƊUb@L IDZ ,?'I \}&[HW^_tUK-7JV{4N֭[ns5ZWU- } 'H6<$da 1l[^οtw|#Bo*6X>\Ž^A!f\i.7c;b&i/l?WE-t$U]Y}uX*'=\5;YY p^Tfw>pKyx2߽jJ3k_a)"W+m>8˝}eCwl?Wo!`4N"kwUrGf#yJ&mx@K?nz`0 lJI⼯Y 1X]HHYKjjI@Ch@%3jDשׇr$I, lo'ch C>4j.h}@w|z\AH 9ƚiyڔyN0LZ/&IjK7ZUϿZJLiJ4zV 0K7q3\q4YT8GWy,i_Tv @)KoPigwZY(:xꨰ:}l׀lLyp/w,Y_TiN֭[n)'%*_˒⛪Y*zOm2.7r.{:{W)@dQFA&+7~vvB>q}xt[/>m  gYq! ǂjK[Ugċ('wzC#>tl;]f?-U5,3lkw}`x{\B$I?5$W` @)i(pѭb"@Ky6(WO |s+Z/2_A1)`1(יhAlc&VecfUJY`mLYa%rZ/zD@!4)ǽ0 H J2 kD1!1\ UTWvLgs%7| $JHÚyՠqt%{Sx60}i}>p)-nl;PIe`-†kcWuȅ ;ocz8[nݺ!W%]i%Ow~3@lJzK/@7w0jуvY3<& Ff:k~SOT%ԪQ{޵uَ֮/@ʢ~Ɗ>cԪ,cYA"Ix{^r)UՌɃ{_P ؔ0Ƿf}y[{a1g۶^rY` \dRI *gqVQx qF qSLضD[P2AaJ'ᅬaEcemq:1m6A>`/'GY*Ӂ0s@,~)4)s7dGAוAf9=;+ɫ`k\ U#Lk5s,DX#P!-WY[T8{|>PU!Eǟȸ7׉OF0HjǭZ&p)o|#tN|ƚ]43?I @Rw1i`W9>/~`ls3*cA4ψkNgjH Ƕ!)$>ی.N^Ȯ){oaW9Q};Z#=b)bZL hNTS3P`XR xf:rRex, <+%m0Znݺu; ~Cҳ5>z[޷vK:˱X; Sd(":rMfeSp˫UJj^0~0*DȔ㇗orixpig}LC /:39h@?pPY q)[?]w[p#q^`<-, \:d>Ѓ^^͆1-3:k5Az}wІR+2i9g8;K]4j07<dK% >-J,|`r`14 PJ <՗(@`*;+E` Jh9ɠOJ8=dK!&P4D?Ab뚟֞ >?W qS\dohѳ(^^-,shdYl@$3x"ǚWRUkQob?*c6SrU}+.~9'Xx1g>:ჵŵݜf5;8\ Mw61@k3Y29ッq@ xunx~ջr'@(bw7ҟdWʓMk_;壤iI^o'hӧTǪ\l׷q7{u;~Aw˺ PV ~p,;xDLܘK-+Ƿ/{~ůyyBKD-hfʶhX aQyUg:B* 6kR\*ЎfWއ*Szcc|@bm<sءo** KS%Zqh @ ="ü*/Y񚀞~)HAdPհ32D)_mps?4?I@ڮKJ͏& geCDI(-l`#wZmC:'%t"f _' 6W Ṑ!pB:CHxH"Q(>9Pmuk~@֊"E^ y01^#E{|ׂ6ަ!b@q|crh󅓏[㳸6Rhss )N]{RO4z;y_t֭[ngYH%CIY<K^oۧ:%8v.lTå)S94/h$0X:l{2֎6=ñ *U5'x^r9㖍 ,՜4._ /~j-)-lZ][/9Z V@3Xdby]S!/zvVZmO.Mn+&/?Aˬc7fI+SėNR{ȉb`=P6 VӀϟ5>Vnp^u iEK^t/|-t1 4 A lV %H+<@;Rπ.0ՂIB_Gg")_8N%T\f2=;`To c8n{v+ՠyпaѦ")Aa!Ps6ѳ)%(8Oys˓qjac&eh\ԫ=3_Ǯ@0rگwV p%Y1Zv ñ׭9;gIbcڟtP^[D:m.@1n'u\Ґ 3QfWzfunݺu;CtojU+$}{@SBUPEJC؜h*itu3-7R2wR/J *8΢.O˝[{ӏ%-B+hV6߳R]>  ta fO?OiU7%ZW(ŚU$mêy4! 㔛޲„λwQ$ b Y԰KFKA6wj`<3MT֯%pW>P@* 2,կWn9s +@?je{$߂p@)߂vnA IǺ9TZŏ4Gj q%DYݨTN5Z7`bpKy;xeЂzA ݀} n UɚB<]ڗAggvqwaP8t+TbnurX 5[".5-9R}dN@fN+ӫ*o1aŀ NX;/_s\]H=7F}5Ceݺu ?WIUI/4WJz]!p_=FEϕ/tp}B[cjd"XIlQ3Ԛ$س#{SVMEsV Tgn"YJyqm\8u?d~gۀrxYֺ:mGA^ W[%ꆥv=5 C8xVmI:lݺu ?%~RBfhKG~KG-l >CS2gǍĹ Sc:=.40-)pb6) 6pHvuo\/zЀXT"rYNG[ܾlգvݩŰ~qT{{C'}];k5'}|H{PP[+UL;y@6_dyp D8/HAWRir,Vl5_fDKPłYi{>65* 赡w:1'\?s%`No)iX)qՓ]'֒TIQR{ߋ}4?(c6x10To4]Q~mgm !?ϝ*S?FySI@Khqӯ/Zn+|6*v}`uq"V6MR!hf]c?TCUA50wJҪǺu֭# $_tD`|$;;#mgIŘ0Z>>amؽa]zJ{o-Qd˞ZH{}!Y{d<D돭w>qY5ƻ?*Quc8V7CS4֡ZxkU7$ *Vz󭧿;k؊,{>Xh$fLǁu]$atC2lհ|íOYVuxu?zPwLb=R|&F|/ seN€fT A %EiNgqlB&z t(Ù+ݮ7T@`ɝ5B Yx+^ 'O{[(i }-@M<4#mg4x.jiC: b^Tv`?Wdk\YWAOU dlMT?\=ZtNn1Gdo?ڕ 5V B?Pa HI~d.Jv0Xut)A8'4|‡iڭ{6LMТZ5 &:| EJtYr\3rsa4 dD/שJ~ nrÓҹ]kJ=bڸS{8[nݺ~SMf߽[tBإ*܉:@0%+=uɣfZ.6+bNzAc۬Ihϟ>knܾͫ*@J8:l}wSs--.88#8p{%=H[v+߮H~p(}5SNpfHTXJ*^ EXn{"3^;8{37 ƚNi53V3zp+"0MsG# T92|qXqa}^*^Yɪ FgDH贗aqN :'-aJΆi1},i|mF*W]]iDK4 _Z=X ^Z$aCTW iN4JơJ\src钫Z,,qȪ}}2b)I[[nݺbI4#wsST%0p9z$WGmG].t4jaTTp$$Hq\䅋w<έǪjpXu2Tժ*7__TJU,{;Y7n3<I4ŒNÁJ]e3BT} oe V7gwB k+d0&^{'/^KiyJ@ %duK%Xo@ hcޯ1 *uq&C%Wha~ȁOJulH%q̯EGbs/1{`&Cn9#53Onݺu{ݒ~foNI5Xt{Wu٦gJz&qߡ*.٤3-ȸm)q'U\`1$YM5bf'apeNB Q[[׼V( n~tr } k fwclf,|(6I4r!=,+Fɫ1y.Xe_{cl}p ?#}e)Vs UlOtPU݀_p9$m;pOݺ~p ;_L3$ü˵גu.̏F''GM&>|ሖA]Nv@p2B :gV?'{I+qhmԳw\"nA䜒u" q9kv!:*ۼH-;-E%}/,P7 `HQQQ!eG/9˳7s3b67RYL }bA23*?H+RHϘe@"Ph)9{LF?ݟN #hENFWb->C[ԃ8>PtSe66è.݇,^6Ow~6 IN Qٸv>*N6ARՑhyuW+P6ﹼ"i=}3>堝ؘM!ތ.BnT!HRTv-t N"g<29ptZ֭[ngyZifXI^^*rj͘tZ:bq4=Uܧ^jŤ'`~ M1,Er1\?6mGOݾuIypuv91q٪4jVucxf_gwܰҍ8P4?VO4 JH΁>RKmWq"qr7|;g_:)`]ck].-f#/}[0'«J WN1HU"iKyt!ā0>Q̶y0jc o\ VtQP#ڗk!O sGoZ|2= g@sr~A7ƍ;F@qhV-|<. Oa'1@4x`eETm\6)aMā<+PEAP@ *؃[I6y5'$0m~J*^O~nNNiccxA}'a5'xy[ĀiKVALЅK6;n\ 8 X2ZHmjPa"Ims?o4ȧqZmcs,0Hkւr زlJ}5ozBy)&)*90/fZjsTPN%z ݺu ?I%Ĥ%]Ҫ{@_6HzJy {[+W ۺuE%B -D+Loq G4 $/>lq 5:J9,Mi@W5 < ,ٳ K,nf;>}[Wk]۷|o dcm/=.c%EQK:4vTW^UJiDT3ez?B_Qx ƹ+0D=LU]  C 1#W_'~x%LVI'(![,2=p| 9Eg^?》Y& 9#@t9t|duo}DSI'06?QeUP۱y0}G819)U p3z*%\!|>u%pXkuz_zXˏIany/4E4v}3ӵ7-P ׅI[& PTE%1dvBM6aiVXZk+,ǯZoq5z͗xف{r߶>/ҿ]8m8Q+Ǒ4;І M Bm [[ֻjdڟZ\nDD=MQ18m?Sx`- JWF}Ϟ${ϵPJ A5 0a&r* UBɻ#mLg@Rd5-s'0O*sI xU:c8GMJ>A5O i9E.zOjY<}ڠl~Oj;۞%p~cz\|"hF# (8S%^Rj v4:&[g*0_! \!6zDI)xba%'1;ֺB7<q2M^Ns709@_ZLL|gpND>dzf>`-@hWSQ1UJHFxpz~blF(j2:cѨHW2G4`UoJ,t] n(G݈dYvj&YnLԿɴoJh O fMOၝ7_FI?6s iM~NwP8}1P)6_4r%Imہ.q8Jek+A7J*aT}/iUt8+XIVln]Jڶ3#輔V7+Qk.h0aZj47$/-zx40@Z'a%9jMTFctk tT*S59w$8u&V?QOJ̸MQjBW} 4r0^^ ź_mFAV卹zT k h3'qOy0cjl(0AJ@Cf0XX|_u N!H% 4.V)ɲ o5z˩lu\ܞ ]uCf,b\}\>^9ș;6iPު-yPF>^3 Dy_e@ȓ'z̓7:Ԍ`Bc~ha? *8%E=-c31 %5Xp'a@P(oaze.SBST?R>H dgdp:R0لD*^殜 H34's-;54&VEA>,@͵<1-Ɣ5Y41AOIY6/!@;\+ vYu  WI:kԁ3IXGUњr[҉ӭ[n3JEIS5$=)UlJyTg!#-H`cú-=1)y%Ve_I%T#@ ޷k*(Lb[Ѵ@mPEͪ 'I`U T#$W :,.;Mwo9^) Aa_oa&zLMX0^ِއ@P`6 2O=1k5>,hp:* XFP-{"C;D  \!S|&So95 W)%cg/QMc "s!pvЩ;jSMc~08֎A=NFeiQV%䠾$0 Xyq- bC?vS295ANX;LH %ܖN-M*cD0t>v; qc$NuR)r@=U"E7s=麄v-ȣ*3ۻtݷ-uEso۲K (5.ݺu ?ϑivI?)#{_V QP^FpȬж"}ӥW4_ "́֫i8D`SNwܳ/3֋'~!94~(x˳/k avZ8| }yEO:=^qOʷhr#R|Nyv Pd8!@DM q5{ΤY@H@?$4L\-|x׏d멺$X[ZL%}ړ>fuKnsA3C{EK8}FTD#]=,&XGl ,`' j!.`,H 1q& ^U cۺԟAcz3]tՆ󒌗UnxϩN֭[ns";5nJIϥov/YQQ){cKB=*)]J\xοlCC`2![gІFkvX1.UYZf?>Z}WD괰i^{72V^悬ç;|g_6w&Mݻm.s ŴZff:Hehe'bրMܧs0nS1|jȨ'1V8XǝiK_ w-43'?T$.FEê;ʢ?/ !6I'Y ]ȃYs.c { y8kA{c01^ ttOKuoA*Ŷm /͟aW %3O@q>ޣ;W۹o[[$fp6*@/FcG~~}JJKbgټϕ`6?<5ۯP)M5FKGTixTc2$AuWW_5( ڽu~h<*Z?;{h[nݺ=>I?/2C;.$S݃R3TST5Sb:o8pZ.ٔ">HAȰ2$zʠ?/lSo\Df0[oV#R U1D ZC2$v^it_wd˩ 5ˏm?Ws󠌷; WMc^=gT:-GF¸ Z/}8iA,Z -KFy4x xc'gJU;vt;TbnUU-@zHfTPc- rAv+ςbE!8&h%`׆qТJ/VqtIH>Q1UnTN`|pgU [Tk.wa *a=A8yer%0=UE${̈́~ >A6'eX)?KJFOꛫdF;Oԁ(d}g4>Pe :_xp۽=)G0 BcN u$9=†ZШ@mV1pJnݺu{AIǒ4?$;{PJzeHFtIKqͽ꒫Wڽo)UIq(W$ ʀT{mPߡ[1\ju8 k a.G¡L'(1}QCj,ҶtP jyv/>wJ?A/+I `fsr4'dQ QtOu֭y{%u}Z_]&{p˫}.r|fe] XB@MC?!Vt?IEQI>80• L%x 7mz9h?Mlrω=_^:qEPN9^t۵1[>O}C7h kŜw)ALOd(dR\i)5[?Aa}?TP\*\$<;PP\W{\ၲ~:pWnz` E~!0?7?)cb:7YCOdQ@-] 5a?/bM8z; {3uuڠ?T^G;- ~q&B38{8sB'dzE@:lD窮~AN+*쾈VW>maAly( GW `vqՏ#u0W^@?I3zJ904V~Q/>& s>~ ko^e HIg;ZpG-tUK-7gc2ǬZӽu=nݺuv~Ek!g$?zPǫgIuL T nAGvtE.Io%av %PALw;as.\[MNu.YmQ\8' {:w HT',,xg.HM2=./<}&Xݘݱ_fijM2l9Ӛm.t.2)sZTę|Z_(͙Sm>㬯ImPp\ۇ揨RP *Sr֑@@y߈6 TD(D^1Ӛ?l Me P48'@&qKmF02> Vȵzj &@YG~4Z&4`Dz-NP"U?%?IqĀ3V{U |`{S0:y6bfU)/|fn/ KgFK zB'5iX*M%F9L4p>4\uAz0/JLI -Is3 X j5Aak7X1ӫ#`m?W9|'$'UB? ‚05K\ W@ h /6LfZ9Yۼπ~o*h29~>چk/aL @3Z1 ??pB'ghWԝV9LiY 27E6ծdžb6s ҝZP/ 7"PR&Hwd`V2>W @2G?.ԎCkY?}!dTKFL|?J%/66L4PUpZG Il.k陸~c?99s(1!V[\O2$4GZ)bq+\$Rd#Y2KEcd.5zؽ~>oƏ*:?anݺu?EK4~o]o: }9nt/" jz^Ү=K!*F4 8-s/|9"F\ق b*GM`dAxn}us>jh[^|kݧ/D3'H ~~T8d!lך#kS{xX?OX0j'k{SbjPa\0huڇ qӻ?0T9X;|c:?AUhwcj翟 Š'5_q҄DϻAUM1O>j(Ta\c(׋(T3eOWTڽ:Y^=(xM, O6茁8;=JT gF;~FEÓZDSIϢtŝ4gPR4mut|%!׽q`?r0,@yǸFFZN$(J L>Նټ9,ٝRJܸԅ.5 Q T:?'{[nݺ:?4Z{WcWfImQquPUա#f2!&HYꢫwm49WMQ![07;=L莿l;ī!<9w1mqV|/~Ň,:t?O:/u_}EO5ƌ@ 8-$A~ y qp!@Nh{3b`\۝A3ۥY# 8h4(8r_|A$G,؍Y-r432*9hqXπ5xx'RHLr5 FAR{[%  l H5 W1yS(kEu}<"h߾(:'^ulc!W9<*@I~0c+=VD$HŠ# v_(ӈQ_ la;' \37>@Ɨ؋qЮsn3 W\^ lt_b=&>?`@gԝjyXO*JH[ߧL芙.{Ԇsѹh}^BЭ[nsD/I$%WK_; iLEOR z|:6`m܋9n, `+6 8f۵t2\_o>@;3tLC4X{KLuW}\WHJ}]pl*Mk\/v|᧲,tI5_}'5.,;+f黴u МF52p 3K!?k_\{vBe;`k{[%z2z*b{ē3b ~Ա__ĕ tVRȄp@leGv(SS ;@g"`- fJ#`_h`G;b[Yc3iyb@{w'3XT) z~YKxI^@Yeӛ<oU Azsej4N:8*}.&/?!Ǿx=3^ұ#myCEݴ}g-Ӫ*;߾Rnݺu?KI?.Hگ̂$#It~Kh_#9RQ*s IPB5tѕ'QvO(PKc4| jFY.R,*~< 6JJ($ >VPRɬ"+˚`޶Ml۞0/+AM~wEȁga)g :ї }[:Fˁ,h~`MFv| t.ToS: (9 b@яQukB0O)<&1nt_}RY W5?ERFCt>D6FZ*&ORQ l~6(*%gP  ǔ_F %c QQB _hAu|&ULH6&s qǦ%A~ߥMcq}J먖buq48}!  ͹s5i q @ؗf RE6t(* I6| f`Lf'HF$e1X)B@'lu~J)80>3(pBdS>`O+ӥDp@<Uw6lA\s4@eR,ೝ|€T'\4VbB*$H3oc5Ud\LpK i"ܓ+bws24}. 4jb jAhH0zޕ$FtVi8ź<T/>9U7uMEFb[|ġ&0@(#]& _ +~Y=bQCV{@ٻ&oApoo|&X] pn?jVd~.8&7 ށoP&LsYP6eQQ.18& #DC|C$ 6A(**Bx`;@Xhm'Pew3YqFw1xW8ON~1h)Iir&>Osu(PD:F6b S- babTXԞD쭍+w4gs\ :H2yN!+Cn?\ȺH*eiIA9QfЍ|m;N=/b1M p5Pv秭 '< YҘ9P l*b![y- ի>p;K]~.3OFVڸS몟0u֭# הos=(鿒?J@ݾ1lR.ipCca*f:u)oh2`4Wq@KcSꆲi9AZEwB/X+@X Kd73󅇨w\xܹDlܻӏjsoSN7җ'L<tݙ_S)9 0k2>>2iY ب*۳dE?A]^HxabU#GssD2:H9NScCQ?%evSËcF8S' {6>_eyT,RFuO;Onf{"Ϝ k$C`{#  U qCJ eIO!#fITBN\#6AglLFX5WVSHh@ g+j{OU/6oT}eXa0?h 6NF{m|x*Lq|x؜?mTmA+gE6Økýgt:px4o9? mIIanݺu?3I_tX`HIt oLۯZRXxͅ E^={Пт}=ه} N4cu%:R1Lu>ȓN_sL)q8"aX^)jժΗof{N"nW#tx@,wʎCj(T}pgƵOʐ(v 9;XǑ4v6 5VOF +qjImkw'bPa5K0SZ0[j(f?{')E'vxwA-vzGj>hzz voF%*tQ#R@7O8ЅB+DI)'(xm(тfnSQ`Z Vau_2=$tm;?K38x@z xz~(k9O;~hΠGsJg)S`s|I\fnoj6:6)xJZIzXҩ֭[ngyߐMwJ IoX r>7 yUebŢHpyI@KYJx~5d@{0ɨLԱS%mL . >c?$ff:6@i3RV?|_:4rC9knkG9헛;9̻ $Rg-,L_ cCJ 窄U0so|,xf9Pঋwx e>Z耐_-`U\G+iiuͤ3~={k5ƯTBjvx.0mjmw>u?VvRظgGO9?s$gAflޮ\< @D,Ƕ@1Nb{mI 5UgWShtޓ!8G` @^Oϵojog@/^LBDx$yEyЖ,Cj2b0ϥc@?wi^ TmHRnFkG3^Zd9%4Ss]2j9wR _F+WG^6>rІ|('$:?ݺu ?I'[쳒~ZR7)̗u/7Re,ϴZ-}sZk0Nʼ7?o'ro2x- >@O|{p'5o9{#>Tl?e&-}Qiq fquV>&6փopk|1Uy Ã|%Cx@Xz'ms~~M M+"xZ- 9˛|$rp3(CXG=xx95>.zOճ>s_'׃r;pi "v00/ f4:HuAc$WZv]9| jr:RL"5D>Jc һ1_wXG ?y v3<~omXO0!/vPQf1:8-RǕB &t3 b4%H :sak}Tm'Z>N'_3]R?0F)jf:\Wݰso`J>-ޟZWz[nݺ=^Ioސ I,$~۷W_G~va^$A>Żn~gw~9`R/?_jۖryʏB MM#<&Mj@7`*_"+r7AJ=E@\MX^ڈ 4~'lFӽA bZ8B4|ľ;[i3ݽx?ݺ?6q O\#:hT!'J5G (v Lp+4.qtHRR% \ YL ؼPYx6`횯 D_Pj#>Q1Џy5t NM>icHcSiR%ӥ=C׈׼,4Lj[gxb󀫛h [$vq=ckOuۥ,; ߱gӅu֭~I? W$i`u@ݺկ^ :X 0< J)6TIyz/pw>yw}Á~q>@`;ֆb%|9έ- oG8ս@9 =-Sq_"a^Mm;~` X&jO%4]{2\cJSO0; < {¶`*qV,貊fp\} 4m% y?`낰rS-Jsҡs]ͼ+߿WPI3i@ ՛DtԥCDhs(Eܸ#nh6K[{X҉J֭[ngyߔ$Mu~۽׭@?(] 3ЇhK3\zhN"T>|ώUkq_:nZv@ 8GL1)jJt(S3ajɌ${LuNߵ8mHdl'LS j@@?eA U?]⽟*W{)jQpX=C9%X`>F딶Y2m]/}$q[O?Ƌw/0 k!J*赙hqh.؄iNE!!r &\ȠŤdu; a@i !Jʚ`^B n>!Іs_IZ ΫZ] # KgS5ycrx:WU+Iy9HzhUd}P+TڹJQi32??D5Y?g4e}AtUK]M-3>N3t~u=nݺuv $7~PK~V%iuTk9˓5g@qe}䞣wC[j޿yѿ5 jq@FA!0`XJ Q2GՂxD߉a16UDp./wk`U@@B6@wzgYήӞs5MqL~YxazR}* #9%KV;>kg}.XNxdE{ز O~//zSZ*(s=4z9%U|/<ObPp`y%p_wgxk񑮓WJhQ}Hc6̌f1%X)r0~!> ۤƵ(x B0(hfy,cP%2<+an@M8%9B[ L+neZL1"?oLtzVmd-yB]]ڳoNZ8Zӽanݺu{MIs _%uvfuT=:i2RAw_՚- D?wlΙvٗzu;4(A-M1,x0K:Fi , zyUTI}E;!H̪K(>6A^,gFyEg!=#~r49QAdTP.I vt@?x|SLl'7k rțK }JcSV?7JI ~Irqߙvp܋&ldq$w#荺 {hP&VY6kC#[mno5_kx;B6_hr-lÖy@T''8/*.h6Ƕ؜,~xWu6up^ۿ>kDOQP MTWjHFZumNX} 08ongU@AF\k|̎X(| X/Q"Tdri<xU5 s*Ԥ ~&P edGR&`mƐͪ.%^yգ\HR{=ECu*GԀ e*+e`jx6qZI6ZtEM& tŊ>#O7@h*͆+ԫ NutF;ͭ߸=fK@v Rވ>7i"mM9;+4@fmmjM_M[ݽdisܾhQ#anQ+ طHEiY #+kzmNyjW}plz::^w_aTV%Ma;h]ߩ4 ]eh m -@Ґ ">Șo! 3F!l5%0k}%dX]y% Dϑ j 1)k|C]|XB*V_C7|S&cXg ?``hkn/e%6mgZ- \}_lƿ˒HZ:::::N13/%J?I vt@TJF\\>W'B ٕeA܀;?Q2V-\+&w<ֲ:&;7}m5{n| UO?5Hfu?w5BXWxb M*Ga3ֹ8=.'{8$;cJyjKħYvH@ QJR 2Yi}DL<`lg[Ņ已ӏ:_]茏^&&grA?+1" ҪvQJX} 2Z8H18NfcL|m`A׌ ҌUN0Ҽ<kPʎP%Ka%x,$ fγNB1'KqFՖ7WMd)v$jvM85^^A% 9UMb%$Γy{_god䫽rP5a]No}y?OzI^Nuc exS#s(r4"ID sѫJռ{]򆽟W&)^Dف*|v"@fb[@t`m&b[]}͞U4xnC`? ܋@Fа-! Z@I/?FH,lG5=mh` GIaj]/7%STRt9,4=ų^Z}Ux3@n:|mGSNY*3aջ\x[VIG|[!J`pvc?aLPDJظPU^;29~cb{ G6XHrhJHG[ Ra1sH&W:GUY̫ѰD(bw?eـ`N^!U mv3] q>\X &V&[v%s?KhuT޿uttttbg˒d@?"iW@xd&7`!Ew݄55u<å{JCCo+'֞?SΘk]R\vm8qD,M# 70؞|x%Y(DҴ1עpǮ͡m&,pkP ~dO]yJcRKƜd ڲ}Dͦؾ[>`nzs Sy԰8s+lybE*z:<ϗ_sb=01n1VkN4cyIB I@'Y( GvN RU2+U|@(EX72Rj> ӳGH}ic'@WZDUP{GS1XbOdɇAs֋(zW^,? | s'!jē("T323Xi6pg@jP R # 5]s,jCj~<hgFJ0KR(~|㵈I<3%T ښf)XV3 a-lמ.7/\@j#\1rOJ::::::NGHzf÷%Oҧy@Ҝ߫Oz}Z c1oPynC ~9oz 5n6M:|uaځ]X}]C+<H4`䪧  7 PmPo?dDIɺs˯9uª$휀 J'ol , G VS(g6cd y>XPeZFq Py0!f(k2? $AQg{_$kd)Y6/@pf*_o -a[h0v^f:!l7T,I;SC1R01đqT&1=INIդ*k<ϙ-i9pEd~yTֿYvttttbg_9d@{>ըPw9~j]z%ڽ{$I:t~W<=@2I_bPQ!'k&㝆g,IqCo+Tm O_zփVRfK4'rld Y0/qsCy*H5b; knU#=[ƕB lϛg$X[#v@`:`eWCꛊ4٠ .qKe?r=y :hP':~GW˫1b d["VkNB9!8 r%jp& j$ @(3fd5͟=>i.HB$תBNz:'52S&ʮ=Hjp>1b,m6RnC%!5&Q Sr-)V78I$ FMklJlS "=p~4xG3]}~&Nq gT'[-Os~ˎSLI?%%II.^NuqܱM۷o$}>g xKW\o~.|mڲeI_}.EՅ^[nUֵݸ@lYffbc 9?@RՋ51k5g~JXck Kw$R,`-70*!憩l cÂT l96[|uBZ"A:=N\]Fmayi?aqI/Z;vX0ܺ=[?w!G* 2UՑ$e^& j5Q&l*a-z7(0TDv5AVUď5v}Emc\<$'sDG{1dU <Ϥ%u E_#d)(n8Εvqa:>{Ez+X^آ1k~{/ǘXbÚDDA1JRw:'﬽dfs6޻r=ηv>dABFat:6< A{Ci:b>>*5/'-s!7*/_e2@ﭳhӾQƦ }bQamJk_ @lvC(@^7RϧLh⩹;RXSP_娬L1I0C|:(@瞥e<>l.5@E@@@@@=?\x"x8K P*_i?"1iҧ_G?OʟBח{Cc37^O?SOSC{nk\vQt p7ƟK AeRB<u}|^6ْ7EokH͐'١T@,Sr2!&*+ПӥƼ(|T;<Ռit4&QTzG捥soML|x*~4+Wb)g6σfRF#!STCPܮSz{Xd)f[RwgQUDR.Be}O1C= J. fX[4H6>neKBL>Mg!)$D% y5vxCV>sxcŒ)H}}T6柨ؓzN '"TP5Njc!U<#OKӨPs/Vcs [͛,K^ ꚼ._Q?I*+>jhѶ1gL >IVL5(Q඄,5@5xuuef@}PMGr* [WSأ||6y*wJ'?Nc/604rq bf,FQIF:$W\3N&wQIZH XB6FFCAcg,Js KjJ]6J`XЗdТucR_0N,TH01ޖƯ)QCxHM E?>Q?M̕rH_M --)D8&G^}0]uN?GF… =V=ףJ[XBh]B] B:SP5ևpE@@@@@VݑP L &O}K{"p8唑8 _rxf,^6[-Po^QG#}1#.m l cR @ ×,x$d~ݗx27Eݚ\ f:XRUǝUy۴p=#?JZR&)!RNjbи<|n%p!c }CA\Da%`(,KH0/ t(|)uk-(4٣=0<"oIs\i_[KF읙DjCj? A7_ [_uӻjj2KE9@J)FMD~X־zQPk>xoҥ\MdHy:5>\ZpzC­jOMAڇV&9Bg"ߢPMݗs+-#%XX֒~%c{~Rd?R|ÈE~PKC}eN~A͑nɳp5l&xLrLmG ,W{"P0z?> ]t`0q7_#v=4ր@ <) )P2BL7@"{XICYLA9sg-ݰK+}Z>Yo1Ǭ_׵1?Q 37d:紻#Xad;H6 2HG3U*,!A +/ -cM~@s,J܅zm+ۄn*&Z[^rww%} :TŽH6i&}wEy{44lY`@6-xjזV_nSv"g<  +2U# 'f 1R\9&9dܑ{K*e}Ao=vMF\C#U|~!PUjxOƝd,(D 2G>2H?pE5M}\%ğ{)%0yc=A/,`D'YpDI bQ'DIG= \PUUc8-K.Y[\Q&Rbߧ!z&~Z"l+\jB (@JrxaDl*=ZtA~= s]?Gu9WbJ4N偸qW?!4Ιm⋯-0=wknŕW?)ู^l*{V&èϮYyS [f[0¬%_WHV`,*Wj`ߤ Zk&KHyQ6W:[D\ƒ1[Qy"{!bIiugpfܮ)Vٷ(W^Qҡ\^#!\_u󦢱` Vm yi_2.M%㭣!`Eז!yCG8LRQgKƗk\$PwIi뙋]@~OJҀyӟY,0c ڂցcIA/D%zLNa=Q% ˘@iļJVU;ܦ8@B) 8(ls”0&*3o53m J>%ꟍB kC*&8-!c P g@O=grbpշk@?Tz98WbaG>KE`޷o {}$&>лW[LFzǝpy@%]k?7O Q;*:'},(!Qz PH{$E>6qd/͐qˢ7Tܹr;@W *2b@OL3 `J[zqki㟬~}:@{>*$X@W):&ka_K ٪TAS;p#rC%ʬ.̍_xDxÑƢePe {SUQ1uT\И@_,-'qo|oA:-BoyCXVư\?I)=p|_#)h cD<4#; KANLoX)'dJB/װPnZ+2I!mkvQs, DsR:3$MUI=#1T9bTo#4SO]z!W6wy$?@2/AV%2pbgdhg$[?ëI) $1|^O#,@>s mT%^?qe2lM;=z&`·1uˆF7 Ho}@2gŠY|%2Oo̷ 90v4֠*Ƣ?EAI3~XoXƂ-ި>H ^KK^J1,@/4ܹhM[V-hyx1UC)xSxJ[ɨ:y**ՏlKN>jxhlF_}htp&`Z,ɾZΟN!f3<VgEhrLGd!N4O2O>4E+珜^k j oُ?&:iAоK) M."}%' - @|͂醳p }wjd|hٲz!U*pWƩ} г$Fo?h3)P?5XK{T%L oTm*$u*'(H#js9 u9l~6t@"Im;eнoJ$xT}҂{")4ƱPj0 @lO&!> ;|6]Ð!a~ҥ|;Ӧ~?WlС;`m_^ŋgǟ' 6mZN_|ErDбc{kUUXt,YE7??ЫW+%/_ٳ%pUǞMO81`!C*KgYH:ԩ7TBF` !+$@x_@ͽf ޞ7ᢑfCR .NmQHGyY&jf{7Wp q#j$A}m)"_cnM@:Եis=8)Wfɪg7}i'U&(r'vVת/aYܮ菕dlbN\*3tr}P4RL{ˉ]"z'Nt+K%SJ[=W5e$ԘER.qǿHC^t=xcQIi"O0lsq_ 6o{0/rO0ϛWQ'KgOy/&Bل ƠI>۔Y!mDC| φ~g'p<M./.0`@ @\<W^q!ԦߙHZ.{&MpTVV;W 5yMũ',t਍L).f꧛Lʟ뮃Q_Rssk3py7S w>|5~Cc]<9gx;0mڗˑX.!lD{Y Dp܋ԑ ֋ Jx $_-Ajp"p T3q3ffݐ/(7hcE Ơ Q#V̯XƂ͚i5Joshv $&A $WmSÂ^3'pU|rOqyIM#Bwph%흀^XKïbeg P) uAXTU ߟ-^RO" wJ7JjHw4_,"XET۩6&jd,OCrof2})D-eX][ @mЃ_#=_v6[o͛7CZLNŘ5{9Ҡ04)Q}@ɈAF݋M`S#OSVۮd\rɹ8̳oZ<9v?7@x1Gq74H;q^Lxh )8* }cTQ(w/֚%rf-X262ݛjnQɻ_}A N Qv01:˕W ? zIb[ʪ TtƀV8 "U ƤjM^[cf_–Ф((kbݼX;֣`+;5CG$Ju)A(B֧)"EslEBjϒXxFJ„3GTZ=!ZXDbFhGW7 8 ?}Dp0|VYc9_:WxX>~]ada\BcJU<}S)FVA_I=Ro &;"[:}IzmA#rƮsh߭ ˆ7"~Hҽ!POg7a. <|~͔{>"kpONoEE⫯jme7nJ`Bx sm<}+1,ZVԩS\wm83PXXXЖ[x:S?t fΚŋ*nm۴F]QVV]=gwPuP<=q@vw19S-@:v5被pmO5 4rl+=i׳G8|=|_n-'djӔ8JKϗϾ1oDTQRf lhN.6y&!  d6nXx佛ZIө,KN!Ōx?:8[N bNRc)kL:XSK44vLf ^pWv~4. h(j?6ؚ*4}lͳVz-/JAIݰ $0%(9 &Y*tyNqeg5d2`!.7s3FaHx tk QG$J2y )WDIS2/i_Wח6%Myq푴x4AHwp ȁVKb} .C1$ZSKe,zmUB)0Go#2!PO @1|7%׆ P 6{N¥uXd)ӯo;(--Z||1ř+͚5E^|q<>\pί-\ <7p^&[:i]=W]ua-555A@/c7$%9w5-?ÆR/OGII1v,]v8묓8e˖jq:a!5b<-*]<U?6woJBi}Nܿo̽m4 bs.d*)7}R=%4iΟ&W@v&@?Q,IJBM J*Ue! :u!@kֿJ%EޔpI*Z,fykXLe=v@i׭a ~̨YD咙h, ;SwWc<8"f}>Y\T|5>=sG">ƒ`uZrF$W5'O!-ԫJ*C}&C=' m`yZ<WW){I''}r( b˧AD␞J{f):0)#{,8~$BAhڢ| DTf:UX`}13cfO%DP V-KpO%BocG+ }vxHs*Kq:/jADQ6*\dRUt` zŀ"xBpE5#\ց&z 8uX)+@2yRFYKN:s$:lEآS)!jo!rR)g&I*v >N|~ $tU*Pjyk߽55yZ7GmkawG>3 ~W!}1~ߝժ < j16=7pˮ#p/A/^>J9M|Y۫Aw%[o8 \3޽ (Wc93f_~.=x0mL> =j0^Shb"O%_k&X26270.<.;`@ ~pP}>!ta_.ߎˮ3(13uXt,~Ç 7mZII><;GauMH.<W_uq@'/t߭øq;'E6p7㎻Ex0\w%Fw6^0 Wѵ@(Cݰ?' jixjkɪt);%:@|Y@ykW ˶Ot˾5Y[)`2kpvI%_]IyǎH_^܈Mx p au`勵 &A. ^Qi!pT=ݒzALJ* _[d]rJf 2vSNzVx뾝 XX@kV*jJ bG%ᒱF>3Ș8_Bhe(+Y O Vi5VhOMȢ(bJ)JU."Wh?H}H{ru7gN B~t 7)iC6QïK?o:r]}|hR 'U=ɂ$ *ϩk+rU]4mV(-ϒ4mdڟEtoUa gIG5;\Ԩ ЏZtyf4.n\U m z0y]>\­=@p{ࡇDZBcKbMRM*7oV4?:̟;^vwkg[m=]w>d6 .%<̚5'{kMV: `YAW&<.ꚠWʗڝ(T4fcxw3W#߮.,eu ߉HŷSBDfEeqG% 8>?(-ʺm f2:Ecoڣ;)l*U"ʏ읥J@R/yԫBkE~k4y'<itxVׇ&xlcsb#;T+ZQ"L6u H\}"$޶KO\"f*@M0U?$Qi7IA)XIŗ|-w"hSJJ×9(DA#퀀ltxy 5=Cpތ:L1r,6\K\x٨ ~uW\v.7chNĦǯ @sf֭[BTqUɋ$ocyjŊo0"=ztw*ڲ=sm/OgIf㦼qUpZ>$yvml iDL=pmoG })r^yoVW/@cK:XB~F&JO :3UQ0yݰ.rڎnVӤ.x?vZJ )I7x!h#yIQ#CpiJ|7JRqc{y^kO4y-zI]).O6)a׵@h,آ٪lJ=(AXvkd@k{csh@2\N| 62 ^~UHԙOrP3wD/G/J- R>NJF},ƐJ(U0%=d >S*Kz>ǂaj 6@~g[y\+UǮ"meBlѹ EYHs$ X Y4 ɟ~n7a:sj/ @(,*Da.BPUU?*Ow;iqgTҩ]M'{ Ǝ}k#=st3qyg#$KOe7Oz >@y<`lоڷqlp"̛|<W[WA#@'?.eBv:]WI_uÛH^O_Fߒmğ #X/Yr92ZaH}[8TƽH6i]k>XӻmkiV?$u)@>EOE, ~  @p3dqp9{T fIrxaDl }{o>xEEEg\x?R|?!6e2dGuAQO>CstL qa݆aƌo˯aԨј8 )tÛ̸)nTj7}fy1Kը y9u#})xa3k!lIꝶK͠_C7 @@SI[0E]9b (rW^㨣$L:U4''Fֺ>+YVyc4Dr vDֶʽĨzw9R"`tf ĕkQ;X?o `e&}Z?\]eS*g pT (}н4S6>̕bU(#DʮP8i,,K~$u[r iNU.,|$#24-q!)bA`DnsK`JelI\8&TL`,uQqUꝫs7ˠ֥(mR<|3fMu.ãE0Eh ]Fy` r)QjVM{+6?WŷU#>pr_bdZ*@(zR(YYm^\ 0K)KҺg>$u$U3>&GW\1C$M DrUi>;>*l* /#8Qj '.ag  >joK \SHzg zoUmKE5Ǧ"|,I|~V1 ȟ=?a_,^@@ 7786| ]tB}bΜsm7ЪUK_Ro H80|^(--1o\vٍxW{(_< t5 CRA9fw`rOcTO.)\½deBJrf[;)Vo6yrXP& nbX˂{~ã>)iW),I<0ҨX=?%S]fhs.RBX$୕R6 vlj׊ISG^K5'ФOQP,<4 | kX?irgM߲ OŊWJ 2i?o {U?k26B;DC/ =)W` cD Jӣt֫r+N<3i[Y?Zנ[NoHR)~.y\9͉lKSI69!^Wu1 }Bϱ.![|!eפu(߃%-D.MIItqy,>? @tp C3a^  $3NFU@}  >Ob9oPQw7mcQκl*3 AMq҉a=*(z97H}o1<1 t0Gu uLmF- pxri"Hҫ0x%^=4dY5G@,Znt^ՔU/Wl0hUU*YFj|z9!$"\hBE'lbNy;OMUp )hEJAXlHXBRFKRrbJK',Txڎ.PW< qO-6r1F~/I(Փ*&%N')m:dѣ_ sMT$03SƱn!`@@ =~ƍ:q]O݁}u⊛p1.;p`SqԑC#40W:{;r,hsW X/ kH0QaᦿpSeѶ&Lb/O ט\QxPg>JZoPђ2)eTip~VsX=_&u{Eu@GΡq$%X/.^P'VW|UzeּDZTϖ\ Y7[.^@'Yn͊}Lcd QN(<0Qxb__/Ɔ3X[[O k!-BvPA?0O7CO‰Oׁfs1)HYKۄBtY ܬ%i{rPˉ]͝/%s0\aԳK*E=M[8mJc 03[ p1 z aa;//ycOēD}GGٿ> ~{ F"8㜄RaUE 9_|GFA<?]ܣE t< ? _ 3%H'~@>n+Qn=2_:Ķ:Ӧ}ze OK"!031QtB¼#MG^¬YڿӎӧI}ѡ]7fV}СlV`yն=bG\XBȟ$H/9G$#GJ?t@Zy53HR&3g_@'iԾq -ic6mKVWPհ.eɀ&_:)7eߘ7RnQhO,:W~#YѫҼɫPD K"[+ B-k!.!&!\o+6L_ծNYyEc?&h~hBYfPuJgu??SEǣda %nH Iitq/$d|cD [ !dޑWtAA̓.Mi"\AvZgj gs(zv6)j},n{v yO o[=>?dp!3F&L Qʭ79ӝl(Q g@='hd30k9SI&~+ug ЭOTT_@&1^lL|L|~zx,KA@)p'KU?X4Uqp=T)qXu> V6r&b9u 0wۂ)KG-R nT=ã:=iJv8z5,avW^:$̺Otoz[VK`U{]qeFRhc U ȬYXa2YؾP0Q&<-\%Eֆ+`3 $Eh_6~@&8 p G%6h֬)0~wY8a_6TbaG;qy&N65y4=kϡ?ݻqym1os7 7VO*(A=rnxhϋ36W) #$ Ѧ뫳/ͪ8XJ^yZO1!oU69'Y\XgI0X E!HA< {BAQk*8, lf^fmU?9{"ע#d3_bT!`YkmVq17!DWXɘd=:]|vI |F0GN'uR1IIIe'9;ljCBJB,8M$s|E IC{m ydyR*5YȜ,n9TeNZϑŖѶc92!nw'N>+C`;l@/h\T&ʅ)A pu+3g@hz!ZjjjCį0p@'ߔ:/_7ߞIj:⡿&NMV]{i83 w쉉~NHq<8 'F+Xp+='dmcȪKf,PF0Kg4IH/Kc|kB;4mYi}(W"UI^Q*9hW\),PYXQ=qOh{Q+E̡jaU*T/A[ϿX'ď BgUpk/_*0HRU(S\3$.IsZJפȺaD>S_{Uˢ4eѥWQu!g9%a (z'JZS 1 h \$R@@ I't}w ٬*UW^[7taSa >c DZqgӏ2/mmp@*cy*hT'o~'n dр8)Z\ @2ƃ( #JH )T[U.2ObD>Z)g Q)Rg:M+"DKXhoCڗs'c,U4kRs~"LQQYI z,CIy!J^<"Z`!0c|O!cB4Ǐ +"}/x@Mh9rp8jܕ]W P Pz8nr1hbO?yλ?\,_3g׾`آ]thڵEP\\,O?+VCpt$:u^k׮[o3ʕЧo/ܲ;JKe1=<#8[whwBƧNܹ05soDvmѩS{h߾+nu?,Yxz+{e"dH< WW~{&)=|BQՋԣܡeďH`!r.ɛͽ̴ǚR n^v7YNH@պI:#W%e7, &5Q@H&A},@ A=ڣ:'e"4+w v٦mѤ5kR~ qf_`P-jՅE( +!F TP$,e?e į[ dRkwQ2^$F>@)T?Fd$i'MnlDf6j"ҥ}󧤥qBHF0D\U?w s0'!JjNt/qWI+ԙeGI] r@KѬU9ej|6o:X 3, #$%dKV^#pt7 P :`>S(N?'<)-[Aü8_v"҈2{ixU4ZQ}@=lO~nj;^=G1du3Uk mLU6.\Bl):,K7< ͷ507v5,5;گh[P9O"Ҿ::(IV6+%0O(AX"O(~:37TM@ Iq%MzeR: x_Y$Z_ Qk2Eei{D咙&j֭DCɵeQyd! +i=~Rv!fqZgYHG=>? R<7I/Cl _8Pu P@?x GZeOK‡ .8}BZ|tzogzM4C_0,Y ]~#Ew`}'HR?r:PvȐ pźu5l c*!.O>{ ({M^/I[Bq4XO|A<:0_pœ{<5[ֹͪѲ|fVd" Yj kabe:jyrTpO($zRL[IwH6ﷀK)'8LTuvꜩ;`2YuݶB\1Qf9*ʥ'Δ73QaSPpH R=^+}{1"CvN=oi37H/K)hh'؜KFkg_,q!*F\vO>!*is/HX=k}K1S[֛>VVTxr%s^U;m)n"W>cmٓ*n%E!T@#vB 2 .d[ L Iʷl\ U '2B}topa`592jshp5Ёm @b]vAQQamJ a7wƎ}.l?;vm ߄Sm귂 .]9sbqxtb*Vy^=O<9M m #=]tD۶Ѻu%$ڂ0oDF(SՏ2]"22 >pGW!x ZGy/'Ko$H(A lHgoK W_jm'=,XG;T~YQi''lۊՅ6t jL..5$f3ц)kt61).G޻hɥWWbWa27 (iuQ6۬ ()\uH 89TҲJMgd!;mx/sTkݱ;TxoDI_'`3^ݷܔu`JLN Qf#Or?($ Sz=M93O&ϜhZJRQRLr벻uɱ>&rgk`7+&JiWaIa h$3@pP#, D*ݚ.9.@mvLqQVDCe9E?%B03gE{א0RXGczsլ=[Ū$"7 [Ӄ_MjQ&o iY4_j'+| .QafrX&)SR|9)"ďF vi 7EQIIPA\#PzXkvњ P@E4+MBa-};gz6-*j{IQI׿@+/#y@X<*qyRr4FI8"pU:=])xկz!y=_x-ouqb%: G䕕_+T]Xzd}}#Oس8(nCÛFVG~x}S zmUmEr=S`!Ua h$GqP#L U@l #zͅ.dм+Q9O  cZǑ@ Ӻ׊dy惡с54K^}X`pdRy?[98fyZ!|m,eed1pW+e #]_ "xaD #"u?p{OS Nn@R6QYZ*P cP'(3\GŪ鯣f2/d\6o?i@N@Ano-[$Ьӈ )?ׁ?"յ26FN`\%-@ӎy@BS`J+/6$峆w|)y ?ـ;|J4̅ J{+aze>PZ|Bl.~A-RϹ)@*9k>7-G#s4TE#*f x+DƠ*9UN/ν}@n7}~fҌO;s|4R8 (Ox?)| JPBN&ܷ$h=zQgH@-d'!@tH'e{hG^9~B#TE p:? g@zPKUxٓjr[ 35=_nag)@ ahؐ􍀌o|vxGJf>EY~D$< ۡcAua"|E1,A 84UΧ3 ` ޲@z0? o`99Q]`r#}x =;+gc|/B,Y\ N%ɐWpI\_@̎ | "FN}'mz6ch+Q.>@HYA@S߄K~SEDX,.Tzbehb+PsI> .*!IahU{j]?%-#~IDb4[%ZsS(/Q/E1|ܓBD%$\@*F 2f+ 7%|]q*ժYR?F?O^)cc/AG'_)g T) ?=>+R sW6 |(#<ܟy)!!@JHP[0I884Rd*QD@p3iVH/1WIJb0{tC5k?qR[L2gWD~^ޅiOyHEL#q$bE[ڌz->T5 L{Rny`M̩UVc`+=q-HW(:z[;S?лYַˈ4qWJRr3Cf}V0 BÃ|P# rK~Q=毐q*ab@DĚ&~t1Qb@ǀ{~ AV)xiS.N*Z؅z'i+oS! ضg=uHK<|'b T) ?/T>7"9^tG|D%(!"nM'u9=1̔5pN%t -i b 1.8qVyUB?ЫybC8\PL/*=xנwZoTgaRtJ\p;|)4y IrL/#G{,%ρFEv/P6Kflzơe@.}%*#_fO|ogYP.=ӳJuN,'D GwFP)$K=J.yE)"~\!?s.M(Y*9BsĠeE(l\+ (5pMd$e m0]OD₋RBE)ԡWQQEv쾵+z182k! 3'qlH`K8CO ޤ9@;I P!(c)| JPBJFd*eI‰ vIH%)sY+=[5C.&$ V%,sƕ0jI4|L\韰ϜiPE8޼iߺ h4/cd$<]$I haz~4+e'AC}gȆ$$ U_2ásҙ>dg{|1z01Lnyַ ktt@Gdk{h2sBs)Gd!B܄j2)B!2W6 S Bp F C Ji.W}ӑ92}}{6xFNՊi m̗Oy1 "ͥek5(̓rT8?$a}︼5O}3侴 .O$ ?/\qAAn E8WKLKHHP".t L%tv!٢SxxH1H8}MZ$"b%{6V ZEΏն>8'Y8G}$1É0순}Ho'BLtv 捛,hrﴟL.mxbM֜{ɧHC64&r@"V% ôO/2 [F%/ʸ:5Big#?b>,/І#!9oNa[!JILwgr/|qUR exЗHň0V6ϩU@yX<ДE3 4%%9 $o[ k3ĔӪ r9_d!ǐ{#, @Z˩G=ֿ:}/8ɟvH ~*^-IsA=I# XsP;o4/%$$(@ +'Nyi8_XIƓ\02 =N$>TO6iŋ^?yN˲rUbSs'D+)|?V"}T/k %%Bu,`qD!N2l|}=j+`s p mJ8O/ wxs*%QxBp'SA\~-+ҁ$J dUX'+qOPaH".ZĊR=rpO\x $91$dHHJ{!N~cC'y;LXR?|͆Odc@a*Z6:ʏ£KàԏQi_?;!%\M?b/5@ˀ: ^z=ʃkg<}?yeA=Ux)*H"҄h.d JhIrj JJ$:[88B$\Lm]+oiw?U/[7~r@$갨(չ*/g6`D ?Bs%y2*T2عka'm; Lq$Ag-;\"p78B"_m+~)t JPB ,E3GdoՌ:vlÏy/|Bcroq, fʣܚ 0~d1Tz`dWs%8A>'$$A8x) HKއh͉ "v;,DEdjpBtD*PcЩ~D;'ot[gA7"%wT I%=Y\胢""U}rr[w`AdY揤:9owpɟK|w[Ɗ^Wvbx?_HRBB"K'>_9D$d_<֛70$ZJeQCgiOU<d!`΋() \Sw(W;}yæ{`a3脧x!p5r`v-qe}.J .9=_!IH7#; mLeop i9eg1\!8}W@ɕP~@,O~w0A-Tx⁀%&BQGJY=G:8DQ1L]q/O#I D)%Q:[W7u*H_8pɟ`CƊax:Ơa"2{QJHHP"<$h90^ʍ Mu%_ìo-xd-f$CJ6N2A]x\HiJ TD^F+eL_Zo]ѹ[׮xUO$#}CbKq5P|o mQw&sff~, zjk\k7q{1c^zTzUx/q?O;"@(PuQqRUF1,s|,IrLl%EU9@ cF>($ .j *NE*W0r5 S5ltln "'#!"Pʧ( o.TkGmHZ U-'5W+ gs zI\!-_c̦]$ghh<:)2 .8HtmRVksoxyJp$t$QBO8`d!#8ģC4~\ :) KЄc imw+7/>쉩/D־ڼa_zBPڹ@G%Y5'фꦠrUUQmhr=9ݩ4&2vX.TF6-Gqfysc*6UrجߏE _C}"9REhO 'd'> B?c$YO/б3F(,9,^J8J|\U/|B :ĤEcH,C\kT*J $>z-!Prx"R9VV?49PȖ+ctm?[DكP2N8O:E#Bq> J' D%$(!ay1EQR(Z~{ă(\ƒqŎN0KXL%XOŒM~.}U@'@$C}p"If<-V=gz+_z-M }-_DOt='}w@}  V3uںp{-kqGT,Ih\FUd: yD ETuQ}Sh S݊C=&ALݪA˾ (gl0!1T3$C1܋1 $V/BF#´Q"qp&qlaZ /}%"[#wo_ :<",@pkD9 [{oE1>|~|;_oH=tOʛ:@yGo#9gc@'d?7}%I6bO@l2r!H;H*% /=ode/jOF=ԾlWoo2$xK@eߠ(Lj4Β?\\@ JIdyY,l/EEL3w Sߍ#bK23a`.ݹСG>dJTB҈Ԙ$d0;'>U'rE#'b {8Z=?q%'q#[v!-Lܟ1z>2hBCJEd^d3#SdрQ,.lSLU'_cNr.8E'oUzj?xO8V'[|6E"!' :D/_j9 iBB"RXRE5*^xׯ`u ?G{JJ0$Cg%E"!/>?/C1}~NKHH@JQHH87f۸5FJ@7SexRbJ <0Ty&y=x?)[!!$)? S1%P*v\RY<'=43PocӍpLu.M}%\Nդ$zc&h'!1¼7 / VpKy*|>ArWmt"kOԘ2h2Է\m/)U|К($ v~`(O؇uZR'$="danwBTzZ+v%eWd.VpKP)kLaU(gy$$SzDN^@\b[+l-;P*WNe%h NH9Dx#/0Xމb8>B) ,ztRX^X&zo` 55m޾?`0.JM i"7agT#3f2oP1Jrz! 2xE02HHms.|_P򷏾̴w\5{}ȟ[Cj4YF\#+IiU.rIdNHS?H)Jw!cȚ-|wK-1Ga`=Fg w/ZxchR Nv=^j{qcVv=jP#m/"zdy]QFU,ud5(@/^ ֌ꪈzA\8ݱxXS]WCH9^24&pۉX\F3\g>IzE BwQ=Ķ~$7PuOuQɨJ%,5'yV'xDSRW`Ayl+[6j \z=* .`cµˤP6| *p&1|ɕ AcO/("^n6Zw IGEUŏ$Qln^ S ! /9[Z8 A 1,Ogy s'7|9okJ%/' *U, Z>_@F|LH8GɟR٧O~+I yh _BBB 6 !,Bq.Jرg(15Ǫh$Td\< <Ĩhu4-oOӱTRJ#EI,;׷ڈH'dE(m$`f GbCbV]Guog]~/A]`ZVn{c'O!5e=Z5kyUXi/Q"qz AB3%.AAD$$$ `Pq Ne0#lfPÁ0_U RNNj-oC B,&߈+z-r1&+JpyPĥ(#eb Vjb6 wHkaף\fNvv yg/T~GKn2p !QA"C8TFP֖.V[ՁU<ɂOϩC=7F}&X8~ȓ(#!a57eB{ɅߓTN8$sk)T:19~*oQx̟~tbChu܋Q^IDH<{>\m,m}XgE5l9ro;Fx*NH8?HH8 lp0`~楄D%$$(xx4 V]mQ;>V%$-7A,1\ R$ Oj )7UVϞ0o P,$ĎLE $Ęx`xʽ<ܒ,!?Ti; *} ;.3فp55*bø?_j{rxOz"pxO0Q< ;rD _BBB"bp$?! a7D8*L%5!qdщfaBP,9OHH+QY%d!^/D2LG?}1p fJI.lqī*_ɛM כ|KJ|ZGrO!e ۮMۨҌ2Ϗ, |nvp8`KPoRX޵[ѷb!6Dև$C1 Tm8`N )t JHHx2l@Iz@F#7I(Uh6 ˊ89ޑ6W@'At{Ḑ:1g> =x.' 9XN)&&O,ZdwQYٯ9AսFglϾ%t37M㍛?r_>vHF[{dtN'6nR@gT Yͯ7iۿ|9WXAL 4|pvAmp/`2,>v P [^8&/љIvQE^E $N6G(# XD $+K9B̐v#t\JKdSJ:߃d"e\L\XJ))*n$NDqYj_$Hbc==/@q۾uW.Bd_hͩGup@p?/W8p8 ` N Ųb;I~D%$$.17裹c"[ b#X|h"'u}c&B47H Wj mە"WMՄP#W6؇Ń8W+< r-ݹ )۲G'֕:|jJXg'mXXyHu]o- ml$v,2Жc }ӻ̾nnVʘG6? r8QE蕰Yߒ nڵ\10?~ 4v9<3[IRu\Z6>5*^+M}|QJԅ(Z 0τWy4,4{d$"_vjJj.ioa?W1`AdE| G9y,ƈOuVB yUزsB |s?fAyJHDN0, W~ϫ)| JHHX T`+ܵ0&J]1{e0BiZmW8"*$H0| 3@" _P u8Z²3q`X&,ILr`Ę>m"ϝo!\×?soAz釭ޑ'OjBrpr*$ca+Tr5py1mH$2l5CϪ(U78{78/& ONH8 W+E"y°nkQ d}$E!!!.OΕT-9UE6G$ʵ .wr",hR%I"AHЕμ>/G{)Thĩݡ?NR@! l*;z XعK8)Ր4MIC3*_|5G&Wվ|OS$?e}I\K\+=MT]0r{pfB`A= up`2:g'ı /}|ϯ:R7 IX@ j rL9(4%]d<3wx !6 hx.t m"!L!>'b\H(dj⇽2D{y?I!CBY ۏ2ONvRJdBXp@ NHxx IaX2\A1B3h <9$oopz X/AE4@jI0v~buPy,4 D<,9DܣQ.q'Ā+"gHV `'1_W6RIipv/]{8Kߐͤ:?ofkrc 33r5)%8&?L?nqs4,1Q_ұKx9ʢs$ #ߕ7WN^Ǟ2ge!}lt1E(1ZO'`# K@\UpMLtΔR>sr8벻2/Jb6J*h1AM۪ظuRl6FcI+8..0_ ?0s/+~cF*G7L#IQNȸ݀Eғ2rr\\ E#S9%O0W-ZQHm2MBG8=2VL$깘G,Bҏ] !ë˸a5BjFNktSH+OH8]8Ӄ }?b=Y4"0($$$,{0UD|M>0J̈́ʽȘ$팱$U,Òt9wd2IXR'I1-M$mUS q̟؇Ns ˭㒄ZkL^v=NpoS#ɜB<1@P /& 䏉A%wdS$XT$!$\bDtl~VNgK˒g傈'! kbHoc9 W^I5R3عw원@ς+<3~'\{ݚ>V&60 %#Jp[=Yy:E0!!!@ VFMx5_ L!+YPNh(bXE@:T?Lx>,.n9q%'(xbjmȱᣔX*҃[BiOcM+{p\\cOo5ut:nM7}ƭͲCʫHe.'a( Cv]SjlԦ=δ_"AҋN+ -[Rx8Tף69>?iLxX*#HSF( x:15\)_dkP~3' [>zX ~(NDzLxa7QDʯj7k9#c9BSBh%l4ڮ2?}*TYLygdgkߧ7n*J#AQ%h`]I y;N NTrf&WޖЪ"*XI(Yp-sz>?}y!LV;'}~v Ӈ~ߝԓhL?3/?yB{0k9SX*}nhq8 d(~""AhR"=DH e02Q)?8c4`Di''N8%C5(ԩq!a{ !H A &R5,.ʰ!8}pL8PTEI]&WTQ l~0c1|; AsJZ^ q9*q`>(L ^JӜDc8{G6|,peMkUZn32ȱzr=`R?#* t}]Txv<0D ?QvUb${JR_EIw9ӾPrtQh|EAJRH98Fʲ+ ;.5"9jKa<x-Xf);O0(/xSPBB…u E>/'O# KE̝8F~B.uM%#6 &d|FDV!A)| ݄mժ O*%n$zh82Jlq 7M zm#=V 'ܓa+&h^YXg۝N98kѪ`%!s 0:TWg*vthL>A9FG`}U=, uz2BOtB6=QDBY |բe#PH`I;.Z7k/BET#.x !Vzx ⑧K1YxNN8:(PdJ]Q}mu8 e&8|~V:Ԝ>|ܱOH8 ot3@> K)z  G) K={`#_4"yRz;Tzjb*REH܉|G,r}L^˪t"Pq )cx-0bMh8Ev>$? + Nv9Z-No$K<| S*8rl2scm HJT[`ei/-gM 3~mHibrXR R?r[^a@3(5fU6[A>?9O9\NKk=C?@QJR4 #o9[GjB cPbD܇t(%C܃)jV &1R1?@?9[*wkoA|K;2)Q#~*1V?QrϨ8,m\-;VRvSo#8U?A>W.2&TpKtt]$!"ckkٟo`Axv(&ɪwisusG^͞8u!@>\ 7{-6cd_O?'9L4cwf{V2ZbF89'LRq"J}ڜY!h5 O,5DOCICk%wBDTM,ӊ sQ|[ѹ7 +h7~qӦϡ^jė.ET>=:Ii3Lqy<1~`5(3vA j:>?3g KМzSO.ď H}rybxZBO_ W(&^*SLCT9SGLcrs/fg11? ;Z,%?phM?# $`N[2sjZŔ- Qh 8Uz#v U?qb\BE Dڧ`0&+ۢ%JTij3{Ͱrh_JGjjH8iW@*NQ.qQq][ŻFQ@A9t>'EC}~J'$t'5sG}JB74la ZQO?{20zPBBB"RHb>Hv9zRSDVćG󄿇FK3*E0),1lLgtY,rQkY,4Q6ȹ}՗F dJ&_y":|E7|3 K?hd=" ~_U*S,:zOBBB"ca$ѩ_,´N7RɊB*F&۱\=Ĕl J |b5~7j#B&1 6Aʝ0o o1CW8pNVc鷛}7ƍVhްi13Hb(.+I.!)MOgz" X ڥ:=ăhG|+qWJP(NiVԚ4//^Q{ Ěk-QzNrK)阄wDb%H#W~8ˉ+|ԌIb$ R]=Dd" +99DR{ ?olka q2~9d`#$Q+~ Mǧ"StB8*8dNsg?($$$ \F LHx8ʇQ흇FyhNUθ eZxrD0CCRv:%}=K`ɱܫ'8!?O`C,@6rQAp;" ōqGf*_=636,YLq;P+uh|BU&Ǡm$!ަJڶQirb]oah߾*ckTtC: nsy?%rS\zgNz;N\]-,,xA>1JMD?z+/ +P}[,* SspiKQ)Ō*'q_ͥ('BBDHP j&yER=r!:+ƭ5lxr8y## N x=`?N{(M<UUҌF-[-a;a`6%a N YBC!``l6xK#Hg[u7oW׌zV眙[~[>y%i;g>_h!ǍR,D]vS w\@@@ ^}F(Մז9NPͭkw S{&0К bdlVZhcINZpUUPb`k^$"$O.rZ< y" $I䙸͸IcsOVDDn`zߖGIDH^'#9mHXA rEDY了4UuM5'aۮ: .Gi|@]׏jqϱ;R!{quP|D5shRBP_*U$ʑHMVq~b!0z'\=d #zcmG5W>G`, ?Zk޼4>~^HRx|c SU$>q~ K/=CQ }Y( "~=ZX}ޑE+ P@@@ U4dHb y6N/<&\T_ pȮg}W~BЉ@tT.le(@@4O[mS)er$Ȩnr_7~f[UfpuSp)}1vYGxD'Gxd$^mF |]" %C<^\ӕO,%OOtn҃] |H%ڃn_yJ *l"AR_ƾF1aJzTA^֓" `@~TA@"@|#sv'BxRp J=k)hŸD/ M8gPZBA tDGᶐLy2EPX~q٬.Y&F."avS ALmeN,s#l8+%mV&͓/?dEJ:53wpP `\APT f⊞7Z'$dyaNM T4R(NbbWϏͻO9 @\0U?l>8!s ?"}fKLB5R}O$<<(YܗPN+ZW'?}!]&$HSwD PΚGyl3!W5RƉJ)Yg*@{ NBGZ"ݶ- \T. Oqt>H t %&; : _jx ;uxJW(/Vp jb(BOcsx?e%hiODyYABs#r%8k9523j=r"8#~'v@u6TOrXxi|ʂL"d +8[H}|eקޮoBH-仆{B=W>_3&\O 7k慦SuA5'bîۯ ,=GwaOhL\7:cS_1{f=rDV\|F)(aL5!A!{aйb{+)oϗINĽ:['Ch;efoNvl]5f᷇UbA(`wK %՘Lg:^nTZSGYQi< k o\qsŧ`. @HT|c۵+d/(QXI8`a6į҃Zku䫁.&/9M$D)y7\M/`G"Ɵ( yvmο[Aos9)CQ>Xn炏0:`ρ !ҲR7y?vx !ΠsX{a`Q|JCtr6ư62OOH@@@ <|%Hճ_z,zNk}ZHHV~j!^ ~սRQ,U^,z"X{X63%scQ˓Eޡr`ƒM߰=#3,WEz m`@Ht)z]@@DPDRr~oWǤ}? @_y4:]{p /@'o{H *>I(*,O7)2QBdF)M$/w %*vi7upǕkҴr[02L$z6*qiV< d% (VK&#'015#zD|눟:p "9zY?u9&@,K@@~-.O# eFrhk nΟHUS 6tDܠ`paOyĐ}HTȩl (Ye)$H$τ[v9$mKĘ@AAgTy>[nB.ЧuqBϒZ4j>tkP ?nv6gfLR7 "ΟL,X*k=B$Pphatd?ʐF1qQr|~LP+}gZSN  }!2G[D>RLZ O*)Sr(}#}J CrSƦ?n c̪uk ;o–Q,z* (t?x?@5< ?r( xQhGT94j͔+7HyO/{HJ6FS(nKW%#>,;AN$ԟRdξCif?˃ёgE/GX{GcmR~_=RJfWg xY "zhVI-S#75D 4vJWru1Ѽ ;a0 @)ߘVqŔ{ȤMUV"KHhxd*~:H0O)UQ'Ȟ| Y>]ܝʢe0~w_y6mKpM՛TN CKI!E"i/WրF}KiU y/@y^PHŽ{5@lpjcm,I1K xzY܁ʡ8B0[-?1 Lɔ4,9.ہ< M]VE9Mht:EoSgov7{Q==p-+my"2Clgέ T&h VEW4ё[PQ@h!m/š뉬waKKI&?=pVQ Z|$"Du`hWDƶI)AD )_hUAFRɲuN̋Z+4ybZTaDoPta='1<:3^٠ v /?\prxM <}&4u#dq \K?4[?w6V_^gÓOU(uL ocT@q"dQ=bmŊ 0儏SIi9 B`KV{rve]c{2˶1 z*jU xlɷ4W| zqرJGh1;ZsY oϗN@l;ͨdE#G+H ctf\Il ~9)`TtoF >64y(j㊀<`~/k}Wq33n]K H'tkB䏿csYSO-k075#GOJE0;O)߿Dzıރ8#F>jGT hqp$(zE`.u^L7FHS)*GhG@s-'iY}rByc)ΪP%H>"XWP'+Ft_ݿ3Tҽܰ7lD׀iM}B I>1 ~mQ}q\$LCȺ1A#0l.H "!;駬㸊дf𵨆c~@\PzjGBYD%'SH1}f*@V~h/\Pn!t "%+^uI#ŠV#A"JAq]l _4dӰ&%ŏRh5Stөq@s'J> rTq@}5K  ,_bsGP&ߞbiϔT2^g+Wx|oRq"DmֹSR'~R{a͓7w*cji\ =T?p1r =R7 5jȣD?7KG u7D _5k? >@7<Rސ. BP^@Ec,J+2PN Jnx#(`PI<y<@/ 9 ]7RtLjzDEǑH1yF:XV]|* O*m"_?m*5;PCzf9?xuz""A(Z  }߄˅a2⧊O=qq$LxnM}sܴ (aSO:Χ|D2HJH?S,9zeODR} ]c]`SyZĺpW,!]%%m`ˉ0HAnW)*dOo;f"gĢ-~@WThc+iNY(F.4}秃:Dl5Տn4^9qќ؊\^s=<핧>{;y_T <=V )(# V@ & wF cK'!Kr2R'Y$2ۜ#zqŏ\%0R\,:[&$F&hDayBZin "\H B'0Âi'09 S_5or/x s߇" `m(_ p=c/,G⇶WK g M#w5 x>s(bdA=E I!y/Ǯ%8c{u&3o(,b|V}9[jIUH6fۥם>++j,C?ZC Aet"NW{(>j`U4CThF1:t k3 `Nd`mGiͨNj:_OΟ UrMF^)2/7iE>wjij3vɊ=ZPNC)|IV X~4Td]r#y8%2HN9*M OHJ EI*.v}v6uZ]aZ/WfܬzF8"z$~P*1(.Li3wR5M cͨ0 P@@u BL#>"K M#JR(8{^/4W(}f*<$Q/xy\TA>DTpWv7|]J0cGfw'@ؚ>+7?-p@esă%A2hGAir]/jH XSFR GIؼ|~Emd p׸8͠}6>:d|n_l>^r{B|,U蹥oS݌ 0ߧ_-cQqhG_,V ,n!Gavn@A?-4^7qڸc7*r,(u=cHZ0ӟi - rZFV́RbRwʒ{7?S ;R r?i%*E*D,rS0{eu%jiL/VWa|`uEI)?]}c56`x۽pDu@ye (Aа6gCYaT7HR]R$ ?F5|8N^n샀btJ3?*|uIc J @4hNW@@?%x ty!!F*:CR9ї{|R? |2÷0)K!K j>O+Flj~N:ŗgoS3p!ʗz06a00hwLeEU hJOn-{F "hGqDTJ46`>{/>}![G ͩ7p4Gkef42$]{U1&{(W?LSAD\%*ci,3fur@)&y[salںZGeiZiC%+rs"\[9pv8.5 ҕi+`^Juب#ء9$RfJi043+ֵ1=/9԰vp?IWP@@@7Be }A0iDy4oe>0_)Tω)KyFh!^?+^R*7ඃreA>#v( F%%lF.2?k>ۅc}teߡ/CH细~sm{It@k^=ױAřm;VR4O gtKq)dAGXthwXXOd8;!@mK5,.~Ou] A}|7cqƐ4 B'.C†9=sFy)j{DXC}5_i<= +z*W2cp2@EqXFIօ-yHTWWYJ v*(Q{%T& `|xؼ{6!5rKudh0fV0 Ö,{ŏF m(]F>kB7rAP 6j5X@)G֚|7EcR :Jp вf#s 7~-( `=x-7 jre,8?P\%?RI& f \B H&d{I!?%\VVBJ Fr:'$ ,tB˷S)M`iWx %13)4F;K59uNVw]<uD  'ie)r '̅$ @a(&X{g HieU wB#$g{(]-  .t0H: gFyA9Kj&NzP~ UB@ 1$B˄1T7*#J>_|cʷN򤁼>4bw㍈#;j%~O=8v<241{ɍvGO594E?(@~NA^xH@mą¤K-<7ďu*DB#hNlÆ]@iUQ= *IR?O{gDs w۽M/}H43?S9kȞpX~_~;x(OJ$,LIln1ɰ{g%g$NP{֓ZG161"?iP;@u>?g_@AO=uYwdUTȎ+k2Ⱥs{!M``W"mgJ6}d KD] 耀ZhvaK(O+:ˬ+J"@gpR1jD|,АM ICmK+rua76Û/B:!cHQ*$M(P+W 1+7 _{)T W}*)+Z y銾p|[q< [_Jq\7l>sxv17C/c~O!G/{$|̮C 5P?`ey{jc & ҕ!z(Gk[7rb|]hlq,{QQ'cM*vw>'ęHSDT?rqZ)EA3*("v !,oM"h S^@,e7cjFDQ>y×8q >?hO>8$C|WA_\k@*C A {7ǧPAÂ#L)">nj=o~p?ˍ; JqwpAt4(Ecęn)#Ryܐ$xIIDLjE3e FI}0NQ]3~]s‹H"H%}+9g$<-z%{=*Y}0Zp׭Ͼr7i:@gؽ MwM5vNX_BƉG-w 9iaO1(>"ͮ#cv/ֆ` lUX;ݛNb4& iz"5Ejը Go _xt@_SD>2DH&%c؅gV#Y[8|bm6`hV $Ȋ#;oW# I4>uk7޴ zKVrGXiJ N9O?\^`847݆v\0YϑAJȤm\ 7BӀU+` lc6$remAD&g"Zw-s1sk U"!1@g]P"T^ Xvbp%xWR+3oq(#AB~';xR@s3?)ul@ȁLwz7;Jk*AuzX:\`.\%_ m2ƿoR]s6S;Q#cɳwO.|JldaXJb |D?MeyA A*=-ıơWeSV<<":K+ϓ,!'P?r ˙K G@"-\NA3MFw܂|9y/7Za j,%aK?/3/!oR‘"^F}9 O_N5nۊi?9$}5KUQn{e9*cWoqFė>8FZd7"{YX Zp@mwO-Dܜk2SA2 />w`U Q@G@TC1,4#+Sc2(h -i NCs5(!TBX?b uwM?5֚cPu'?` k-? t\(C+c}^ 7!:t& !B+K(DDZi b@F0@Lxj)&f堚0Bq`i{(U>@>Ro7'!ȮKu)S9}G쭡R>>ve0z)=ǶtӂdI a  vfxD{ol x9A]G{*ڝ>>it@iBCG'htphGLZuD GFМ:z!ndN}d{a Vɱul:%qBˠsO=lh)N Һ>NP*h;k~/;O%)P椏 /1'f}"TP뗴82RqY3 ib@ ރcߴ!JTx xy@&=|o_'vQ9u`Shݚ/J9Wx('{bMrN6BG~Lц6|:gGWEY^ X"?o ^j;z8jpb'/l"q%yi?4 `u#3*A >hEgT;~#0bB8zR3B<=LT"_J|8Dp5 ^P)9߯⩒PަfoU@Th]>l2w^h$h6tz)A4 ?H֩[6qfno[?1fehj{0<<89g_(eY^= BZAm,/ws#>>ʋ/͍׷bP7#5Ż%ig.(H0i@͟,Vf.S29Nv!3M&9ay#~}8w*]#Cӝ4'ɗ6ԇp>Ka]-lq q\+UpB'ئs^?yxu x@;kvo~atƤy)d2QP!0G뀆5SXF12# %g31pW,4 u E}ATZԲgq}Қ<"s#u~@\ P^ kR-8Pl(04p< (H(pRҁ6JuTID* E'\V*vCmHmRM$'Ŭm6`O~ɳ 5oq<=pcw3lOuaˁ|6}&u$qRAC5(fJ i)v61Ԋp\CMUGO13<5lI*x40HMqYiNGCgEdic's?iX7"č_OiUmd(FU,\Xe8b0$brZ/ű:ELYO&9}5X>ҢiPKUDhG`L#{r;N PJWcoLM7G#=q"H׵%v>|ƒ*褅N"I,#~cJ(H"X+gܐ@b:O>IJ򩋧,zGGŽh߀\#\vr "(quV&GͤPJXܳBP @k gݿN( ZCjNq.IqyR+iȸý ;J~tf:"0x|/ א #x_P隤r*1gKa_Sܜɻⴙyi?~ ~q_*c "蒦{;vP ZAifH Az(Md FcͽjVz_@k[MSYfϟJ ,,uW7ܴ:m2 ď^&Jgd--\(^x ӗzo7ePHRJ2$X2"ܹY( XZn?X: (?`o Hz"l̠bʉ6ϒ P{p#ctDO D_K%c̅׳1Н}Sţ0nDTdߎrD#66;5C>: ~~6&c><s~RXʵ'Z fGŲ~e2$aTTk8ĉQt,BpcPv}bdб65A)[>XD83B<1*! ^KH>Y~!q1rt(:!Un)y~ E}% ҳ(R(Qy)ڇ+~ը,RQu͆/~z<(V+@f ZAH9ˁJ1>s{+FG 6fgtjFx+P< O>pӞ|:ǧ>@Q䝈KSPy6^wzSŋWN2YdZl=C&e"^P BHh cXz7Nr_Sqk(.Sm>1^1',S2G9O)aWBLMʦSܼ ZGe#A>qpV' @r ^@Aw!nNu@DSI dHkϓF f|NOFC$ATEXwd "l'1ߡZ$Y59LOws?w;ס>|~>~W^:nB6ImmJH @ ;I *k:Ok榊 _m~=|#}R(ʒaÒ煂{0O_ -邾EuN%>2|{uڰJ#!OXOl?b}q9 'z:AD⤆Z^Oj%%QO28"XfYjKpnz͠լtp}~WO[_\c[6-b-WChl؃ bMe)Ȟ(`aLDD&w>@r5¡'tNq7:新W~O&Du Y?&x^=Z^4ƚt,K\ߌaΝZos __BY#FC&Ejĭ:ב2F @d?4 ZѴHC]C \=xSm2%sX@g#9{F4Z^=|_iLDϹto ǑL2TpM# _@r$ڍS['ָ24s!GU_r0F.eg1!J N{$ WQ$+N^#X)iKpuB'( `p?Wz&~귪iD˼=hɕ(#r i# 8JiĔslp#`E\ j OG {x#0eO#mTQd~OPL?O5/VcUN~]tڨ?PY}%= |Wm< ՈIR+Պp12i 7P`9_Ilٴ{l_F ffUc_A 9Jy}~d<'[8A,zgA8<Ξlw.HX"B96? @ 8,HiN8^ bC}HHT Z\~d0L\c"k\ L <>Q=otjϤ8F'] S dKUB툟s5+ dM@@{Q߰|Ǥn506s h4(B,U#kܐH֛NJRj~?PDD))V>Jxnl2XЏj,U(yE5)\( NP e:gŋb?4ä}!O _HmXY<tԏ`@]'n8 !>Cn"~)$S18Ѯ6 Z}ظJPEgk΋~h,wЫw݃S3)kbY ~x&Nl#5k?Ox@8{@=`bdgs 2q(}Jw? R8p^6Wx()VKA %"_i 4JZ%֔zrs.LiÕ>u%3r˖RkDIO5&WO/~J#lTer}?߆{+ҵ'ΤI\+HZVK07ǺB v,NTkK")Fx7(ҏg"emm) }80ES>7"/AfI%֫ I8ybHNԶvBRkAX9)0 G «T@@@H Tڇ]51ҫ'St)?DD. 4pB"`aݫZLє.{74<:e;^e=󝺝#?b+A&JՇa+z |7߂ng#n=pXKm[8)TA% g-`EfQqHXow-. ،]ch(ß5\>|~|҉g[8/R0[5d#{߸}e9,ԟ+%97S%;u%w1DB+_s%%*inI1;4cρI4WWvA#Oϧc<.0YJbI(QM ]K 'ZO]~^CSy > 0ƒ^I$Nv 2h6<躒 FL^/LqG6r'g}DQ=;:69OEyEj$`y^*1 ~Y$q};[n(JND(N<)L\Lqd ϠqGߋgx]ds? 3_;q#UaGj-ﳉ1!VX_‰bdEC*@ƈsR +nIRKEey~~~cFJߧ _ HJpurG5ްwS۶"cPF"??^.`>VN}fjP[pqcW!q=)E^@x7I!͏06VqT]UqӜ D )݃P>@LJ#0t/B>. P( N@ p叜KkoAzR ^7gN :?~- \%{()F$z(v\` !Cz|;u#c$tVl_0ٖ$ 䈡*AVGC0q~oynapd 4=È"2<ٹ bH'kO=XG/,EMy=ߚ:<%"HVS|1fXIv(ZF!'˽p~#g0i_g(D) E%%oSn G0gb*Vr'И {33AN8^.z!V;gE֝C%PjJ6_mA1ZۈapǠxv, :Y)r؜ L EPYb5#.>ƥ. `PRoRj@x8qjjJ~13X*<<>*)d" I,ʏ'8Tf³g)PY/=AjN'ZFq_#Mq{}ȯ?h{bt=/ֶ.RQPƠ (lRǏ~xپ>oca9Cn,瑧mlOkG 2\i:q91Kar04O^1ģ`(w#X*7Yx]7!\%H[[E@,N'ź9FsժDP9F LkCg"d2^$ :xIR:i> v(]ElT=&O?6+ߚbl>+,gٜF0&c ?SySiYYTC%֐5m?`:( `Po_P}oEƇ,sӓl2doY$9j 8Rg{ ".(u+xYH?HW,I#"}>u`q ̼'IoJ]'21H H*<37LseWy'_SN)HStGʔ*>9"y#Ղ` )?MSH-#R'Dx X'^Emt> FoD TTz+ĺ)S8~"@1c؟@>WJH3PYqAA]˼2ªi\+Jڊ-cko_S@@@ }|@@@UݰA(5UEH 'gqYZ= eB<y 8:c }?ȉ* _D!wIy)h ]:)UB| 9⧁> Zȿ>bKi`5yVM 6݅׼b3neqQǞdP=sdhnŅ9$I{PE|#Wg&7 u]Y柂I"J2PstZE 8m3BA>ABч_"r8MIJLAwTbO^>Mj(Q-eXCS8'K?GmW[n؂(nw!JcaYx Xo:w.#/<ϺP q}&$CS\*7Rf wD %WDQȡy<98b7'&T}#j:mr&!r?W75hߧ+ ~? |PnC* )f~P6)`]hNz=wR(ҟyn%oZRY֘eI|a<)e;ɤX|馽{ݻy%DRlK.rPer\d׳eKzmIe , D$i]lN7afXXC z'ܝ9Zuuo>(ޣ y|"H$ @H$yhcj Yi.9Tazv HCc xܕ3!hxGYb.zPҕ 6랡Kkyv1}0PHs[h\;7woXY^yez]ˇ>Ԧ=M7q4ܹy3flX70N^evXOfyRZan\%RNظ3/1wj?o47ldny-=)i~x u61bX~` A[D"QD"`Gm< &P$B4O&h:'^uނX耟nV/ԏۡy,NA׶|wO=:  ܏;0Ң7v7]wn{F{"FwZ!Rp[+ \Ʌ/ļ oBGcdfZ"yfnVɧRv%PP!ȏs@m|tţ$tw1NgLsAA z]++,w_]zvώ05y6\_Y%p>?֍/"bzx10ϵ;h6,uίasm?$`spwoyc|Κ=4.Jw}} |we"OwnG+vhIm;DŽ n"a^'Qg㞣!…#As7$lhq,Yy6:B'\?s X I"Foo}Ƥ׬bisZ*'ia,bU׎uªmAp΄%0~R[ El}DO69 @,3@u0$l?̈́vY eү^ < o]$D(D"?)pz-0!$jD07==Gu3 !yU*@7@CՈ9c"j,t`Y*zt7@ce&Үw,=w  "3zt{]VVV辺 LMa7 [شqwm[xh 1da-eY|- , gD"QD"(A@ eiV} o1xf t=ޠHVRQLR"A: a"X5@V?xVT"R/y%T |lNv.LKC 4?-O_}soXCp>>-r [X5y,ydoq~1sOa/` ڱ~$1g[npY 8}G[TA|.0*=k=<k0ǚ' ?z*kȰUfݳ*LΒE??,"`H$Z?K=t;ۻ#3[G(V0X:J|2. dMlB&"%Ƥ5jņT#hK, zƒ@$AWy8ZE"H"H$s#a`c`wyۡxTY* _™@ kZox8ts悰4&eD # # lOʥ: ѿD8xdptG/gNi @9rd`PLej, X7smY75˜87&SomfkK|yˤf2z|ܓ3S-5TY@MN?>2y$ip8[W)2qaJE-it;!9P#jQa|V!a;8wpou ]):Jxd͏KnyCJOܹzV3}4#ck4> =<ύH$:?)AL|X>_bU;hKȗR4H\:ڢJ ".tKV\yn tin;AR +Y^H$H$\L`0pfab$`Q!֕gix+hEQIk8I\}CSݿy3Q( ByCSc]*骨dxDv%C bp^۩ㅃ纉Hk(,?J%{9<'/rz%P^GJwV*8vIkdM6c wܲkG:~69[V~uW>`%[X\;37.7w\i?]BxSADb_Eb1=D""_/]!xV48wxVC"Oza>[I wO2v]}gnQ|GC.p$~^D"Okڱ}"MKGe^\5qHlB=C;G;-z|KbRȚH%9q@AJjL`5D4~%f?I>| ]\ķ3D\i"H-fz'"ӕqBjBN#v>L@ݬRt0 bn֙ g Pc]7Z10BH>Ft֢ 8Ag$wأ>/($i<ʎm,be*]O{e4kG,-O8ݺi39z*6P1(~Oۣ߯Xp20;e9yٸq=g~vVC+wܓ| Hjck$)WNA!\۽ K}*<+: 0ԧ?n⎷~;66|oz:khjƘ0K,GC43#+M`)Tt|x؃X1\ Ġ{[tyKd6$Cz5܂y\*8 I ⬂J(Ǖš]=Bt$/‘.-IF"țJ"H>p!!*|g#RJ.ZzYAҚS͸uop$Ǽ M {*4;Z;@y"] V3` abyn)\"cv:c-mfFK+O78̲P^| G%EQTV@Ag9$3S XXN0iFsOǩIX{H.GL'V?6涝IҜ@2!}tCRm}tk)B/$1rtNR4[H  &2tGWkEzw\5!ny,8>PL/sx*;D"7 0 * 1Pos&AR͉-tpb;Kٛ"7@Ҁ Y 6$)NkCO}W3B]y?ܾ*: ,UKfK{k9 @*m7 E/KmD".D"M ˜oPm@$mxK=|ʄM@"G Po6v[W a]?l0I`{տ'^``,ԳJxq,AЂ bvs\q&VV+,ݴqR~nwlB#R^7(K^{,[/=v'Vs9,<`EKn==2oK b ֈ* )+lhoá Ajb >:4-lBKzn E]@ VV㶭Jh /J]p{/21l=>_E.Ě?k9qLlD"o0T8,VN=@xNPkn57 c,zElI 0zL$Dt`"!t0@s R:yvSJy`$wX %[ZmD"7-"H[51j SX{>N8piPF d-Ak1 m 0 _od $O8] E @Z,❘E5׻#,A% {h6 ?ll5i4Z-:&fWuWV5G6_Jǎo< 3)HBȗџ)ZXOk>l7s>EN!H@ U\#F8* (PﻥEx\"=<#-*l#(&/>չ =PWo>\SS&l~I§qKAZ?"#-݀ ڱZ+X<0,ovSVP%b DZeK}B\ m`>/ i`҆sf {%jwv.OlzP\sAӂЕT!B*F9 4!Dy08om$D(D".$c!Wv f{W㻄DO &֏2zpAޡ@J iG 70GŪ(P4fGzXn" $A--!xxuD!Π]7ؑ3""^G 8E!/toW"*NU1nqF|~W{i}N>$wQM4tKZ-B*cA7.癟cnnť%Y^^dii. K}Q(-!Fjny}7(G?ZX+|[VbX鵹=+'J9XR &P=cUoyotMoSA$)n.>->0F bcFd5o}yov4m? J5\_LupxAѺ9]: ]k_pRR:e3}MVz&GP! 'Sns#tp1^Ώ 9|x2?H $ 0[ * %]ӄeC'x+#b rTN'hW8J}k. v'6^'՟"&V\*ʸ:4du]wݤ*OI6`~ܤnsS|o#H$ZE"HAx?p3G5 j4oly+%`\]K}p.p azDS`V .f+.$c;&\W8S>Z$'e}g34#9m-o09Bŝ;܏ųjr-th۸i{_xޓ|{,uZàl y.X[& az GNOէnB),>"|~d֬\7']*DIHd\Z rc`y}-=w'κ"n6rذk#4Z0Fˈ]AzM.TO .m=SM̠Ydxӭt'D"׆kAY~gH=}wW@84P3&ފ.YTy"U*XD,b-HY׸@E u1}b]-kR@j,[j:|8> J.Œ؜FSGa$ly%@hD"׼_SD"َ&6G'I#,O!bz7pn6N8X*` Hxú^? [uq և3WV,Skf19.{RպDHy2i0l0Ǹ 6NܾvHNVC٠n;iG/ 6MMJFMli)–EIYزr/B#KX?UzppZ^bzsO!eBwok %1[SomLk| ycزxmhyөƁ;Mu_(JKN7_/n(N 4ac6>_< d=m}l4ukoc VVphBVwv$gx4,_cH$rm @ XmOMDD/5^oIGEB`4VHemb{Sƕ9`+[xxaT{_fsb@s$0!.Tny?Nroq$DpH$ysp pchY+(Ƕud^t $B%IAH'\{7 yפxq4U<+!kCsHQV> *+I|orTn!$ޠ"Tռ ߝ* ŵin1NtqמP+)n &¸ * ƀZ۠D]z~wl`#(B=pyBXN9< bD=:y ZxK$mMf,[c HG[?U/X 2pfxkpiN\Ґ"$0 j'*MʂfBe\}"tBf1L)H679DD"7%py=<Nma4FcI< A (TX18n7 tzA 1S' Yfu}N!0*5gd]o2+̨7A|] B7H,*hbes8'f1ͮ`b%/결KK8 {?=OajU$ZLZ,#S-YQ>ZMgf/ip5[oD@Xϱ| ˇxd#gwu95 ň-ᶓDFe0xo;f:AHLna7A7mG!$wO1^3DLsj5'H%4/ x#e&*N{ؠF L]tFԉ>í#ØDnQ$ZbDUԂ¤i0 0^(݁60 MT\=A塪G %je5 ,zEEIhNwɊP1G\nLb/ tO5R8DD"7'|֘Oa(F4O%dɲ5`zskNQ_+"Pc߄k#'a=X):ŭWV?4K$>O8ALZ 9KMݐFG"H"H$(Bd5 /_1" qݤ٘+~j\ .UQl\aϡuR$Z7PD Ғ=RnmxrXn2/(V 0{Ӈ42/,ϝ+ğnۻn>9aˬ,8_ A,,.:k%?w S9W$ۮV{1)Vh YZvY8f^M0 WglOGo/=5-LǦ]I osK`T~P ARܘ}?MşH6NRcíhQROC o#ۡP֟B ĄԴ5)Dϥgq]wlcfvܟl[TJ|\>E2n; Xa40&sk `l2i,pk'Y>0`0IaY Dcĥ5X 1**Ub :/ cp~ޣu=py0HI]@^cۤjnwt:t87mjeMv鳼NJ0 91s!/JʲDR}u[(.`̈́r(ʔ͡3"6g04 =j4GL:&rUP=9Ca<`,uLn EΙgA_|@<+1rrPysmnbDH%2(gʯ ?F .IJpWg"r`)c!mWp뮍RºQ'1"tl5Egt:/rϺ]˳bD\@6md, `jG? |!-_ZPO@ҙwLӺ6Iݍ%Yo0^|g+A1谸'*d]v]7A6y6Yn/,[׭  ,,177Hsocdd4HHt:M 3N+\5YA(uKV7H#K/|N1@ ?A[L{޽?Idr-o`bn;${K'^QJ&tޗo؃MJJeln6in7MWVR"'~W ԽoXkC-h:R3c^(%I ;n\Þ}BѸHRE0J}DޤldT :Y ǷkqؼZ]@-k Hf5NP7WDc_o|V%Ѷתķq&FWph]ڜ E&E=Ƙ^4kXX^, P.8-0揁@~?}e}+@Lڤ5-JYX -u7gh% Pu:̢@늵A5ף^[}k$[=Ҳ"\(sdc6ᬐ|V$}274 |L/Kou$D(D"o] 03\1C X{<_1cn?UyQZ[ /9_jXxki.3`!H/oKWNz_pv\UU~_K9<~Ç2`frK[W=W?qRf3B;ڒ,ʥ[/'K@w_5-=E={!ty&^Gv!K_bs\&Ncs!( OFm ]]>8LpfJM, Gŭ>վ$‘A3"LN|F1_h;ϥ囶uǁF"7[ TWc!0\ "rl7..cRiFpM!DNjm=hܷUH Fb\x3YY/&ijA&5 2KRb&y& J9i ?… @v= UD"\$D޺0p?`R "ayyp t'Ҭuф*T0D@(%O8ԽF\%ox~0 /4Yo5svsK#72fZwuጏ5ݛ{obZp^:q㥐Hr2>–B#i Mh-4cdC@ʄNF MjR9zf ~{H&PĚO9>߹?_Cglb 5h@NWTiE@QmedCt?mWT d@֓]/W(HB)ԡQD"po> ;gn{MC\g7x5OBh AZgyQD"o ``;$q"'p׎-_Sr x+x?ϵ@(Ƹ\8tsƸI8gUyFNnxݺ@U^=b\Bo#0؂@=WYpyrd !2:7#D"hD"oւv ׃Ï?K$tS䃓XBX#E&p:RsբZk`\=xDYV~{GzRnAH=k`V$- "u&J1T:/򉿶f#byY<2֜g`L2`oGcG_aG3:`0(YY,A?\eaY e)9b1_XzKA0pYG 1Bk K-lT躭$YeW?U[22{q7]>zH AL!3[]LE%!Nj7VR^u ևn0uW[mmZP?5qndrj#I|S} O/H$ֱ CŸ ;/mKGѱm\Y{5U[iLIx#)Ak o[8A\ ⬦n4 )**O0~X{-}Qr`lΥ`MrdHI}C> |",20DT}4p2ݑH$rma_O!D"WSr6F[FrkX{~+Iޯ`Wj<"6U? : < ,3Ui2E @xe`KGNz.5A%LWW8tj{SoV?$D0&E@BO^,IZӁOe\.97GN`m"C >D@Hǁ`Ƿ;DD"o]F03$PG#=.deYxRoC88*R@GEEвcAKڼޞ"wbij 5ޕ<@>p`~PMޙD_jd>Fp4 ?wo]xZZKiU6X^8CcF¥n>ΞTŽ~h53_ԙuyc216 Bmz䃒$5T⏵,3EN_{cz˥[srޒM4GpH&gdj`PN=56+8u׳6m '>b@b i<ܖ=}ܬ]s$Ғ~=eZֺ~xZ_@aضkU3^ᢎb$a^Pׂ~2y7|+Idr`ࣽϾ d#STPgUs'@EvD!7㬂Hɕ@ /mڸ cqi%ܼk5ip+ګc`-^I3.3=nbڥ;w/%{BD~PD˥ce<9I̕Az;ڜ?Ӓw_Zu'b`1Lo"I^[=5Ax xD"":`RH'~\!D`zgBB:kH[H1ZM(ICLT\%ucslѯRpmyUى1E,@[ع|͋`~ oD"k ÿo)D"Hbm`&k46.XlyAWiC ]3)uۇZ9wnr|tYWo G[{j,TN -_!Gz>~o|,<i"k4u45mvnB_*֜RعNoJ`6eӠ];ְdl۴];̟MXx`Z@t`-ɗ>_3hIܷm{><ѓ]"*>P%l<6{lʹxw)1찼e Sk::ۭbY'NkjVKs^|Rʀu"ds۬]%NH$\ |̭ $ qn\uj6Yf+rWr\RNT269>NReYD$p6jh%.EΔg]wP wn^Xܢtclnav+#\|H.,kro?ZIRYMnX\ٱu/p)^cM3di‹J3a<Mq<-vS,=ɥ͇J}ӜoS&f8?HYp*Jb  D'|]5ޫ# $S͍``X$F0b(Pw/nԏO(;!/Aa SOκ6A$ -cD{?,Km૗ pp=;RFc dNJUJR0ElTiRԺu NHẙmtXF]q~N V[}#%//zQbxGs; K I\c$ȐX> 9BփG%@1=$mqVsKM~#H'} E}\}`K|>45/R# H}֖-I[}֔m(ELn)8@pn y׌D"kH$\܂!`uV/ uZ-1t|} AXO@Rۊq0NIX9{R>lzLbRʝ"-AHN e_ZYXw$/ b@N5شé/.fnwV2;=cm+\ Y=}# 4G4 W*HUͪLQ c nX?3(~cOr)kاbj AΩbu7NVZZ,\ _;w_)]_(u.k]A .I+w' ; +LF)^bzKC"E*eL6hKۯZ^_kD"Psu K \$kww :!"gܓsO{_‘6FN4od Z?ף*⏨{8L‰?%"#\$`i6x~ 7U#H$ @H$\ 0y} uF ?Ed`0d0TVR[k1F1ѺlYer KҼOnK{)@YAl41_Iա_ }"nx{~ȲV;[}n7_ $Gy"3k[Eݭn]H`mg?ʌ؆ycac6 dV!$! $Kڭ۷^U=T'ΉqnZRsTFdDr+?ߗEA9Ts֭j1؟q4dρVE^X)uúՃ|߾Lr< aj)chX04XQܡv`4)l>}&yYW-N6NOKZR2up㻟NE{Xvxw^scJ?#BUd+Ɏ 3/dmK߹8# G>?x4AZt\Cœ?"B4 \DCZV] ƯH$۾x'~"ȟ8m/@>ӝܷ}3lwrE Cd&VU*>|[]P nՉB>b K)zT/wpfe8  H$9x"H$r3׃U!6" hA`n]AfPvְLxZ?r~G]Wmnט}\ TJ)Vȭ{(JIC.^Mi42YՓdx9N,t 9EvH0cGj'julۙ=28o;aD I>"P5jf#Ș{#m=h37]w?Dw!N$/8Ex:#|Ć{K3 ę{ٝ.+H͋?lipFi h|m݋;O"U%$~|PX.#FcXu0C"ΒG+Em't[W$&nPus)5c#/ jȁ䁍hKԬE&[I6-n=L'ʮZZ|+_]U3t&AW0)4ՑH$rJ:>B$DNV#:.B 0B *LFU![dhzt$\o]ݭ Bu~(5u7WӸB !B0$q; hE%ibD, sEN'gh@X1.zc|³P%=1 !MZ5r TX+uac?`r:g@vhggʉ3]MaS&خǙωBEG-TjoBi(_T_Z6^g̕(CNŗWMTg~d!+ pe>w*$t-YǂIV :R Гn3ǯH$9:}W˃I܍W]N }@gbo#w^%m>N֚$]lPU#&Ef}jѤUZAWk >P' w8jSI Kw#H$:"H$9 1YwrQ%/ўeR6cB$DNg惾XAb_•0O_~vcr(>{]j2msm$ٯm5rK+#$LkZjD&5)~in##1fmCFi4ڨi5&KZf;Ɔ쟚n79yF ^?+7߾{63g9tdC$&%RӼa^CQ;s ՘i[D w8M}gVж5/ X@0:],7^\O` jǏ_.mx ±x$خ۴B@zxįH$90D~8-E{Vͪ ʂba-01+ ^AZD zs9|Jϕ e w+*`$L_C )BM Ļ8DD".G:(8ah7OH+[jn{33S%7D*7ǟ0(Jv>%gu  ( ss|ŮQs墆,sG>^Ozb[YFި{BPєf/<\CiԲo|;gœݚsqӷ#hTadr,)i^4nyzyuV.m$IҔNݻtaN ?=tX[8fsGĨƒ "R.;q[q$[jW${7be$c;`҂plzu_ף\Ro Z)3vGM$p'@~[KFxskxZ8'sѸPk+;N$&R$V'$yE-" .ĉ=>[ Ax7E?n!]1@wfho-џb$D(D""FBRH4pP=|=nKPe7k(WԻDü@e+H:?E~QfŨm5ܢ)0\瘏'UOu1MIVzYDžk6m^7Z]$%}S{L,1g,܎N )i5I[pI.8k'I#t '>݇y~;GY98FITr^D4PqyE=]8OHWVB+ps:*s]Jw)Cu\J ,Ru{KRa͆ .Gmh)?2Ǹ]]%DN_?B;T> 4/*hCuQU"esZqc7ʺm/LA۷?VAD(p8U@9?+?CD"S=1P$D^,y*[U}쀢Je8ϻtT,Ofݮ?lsQ(-!XM+qcJ0?[_hgƘ W=H۵_VcuѤ'y~њ#vo>< {%"QPrB=0pZ" lK$R?7pkJg:StkŕSxVەPՊG)91܅1\PQ\O(lJD"-}2?1O:M~8+kIQs ct |x7ρU^?TX#v~gBo?yUo}ηxK/@:Հ $-@ xpWk#H$ @H$ lQ1 'B خ֒ۘ W(;|j`2)oҺޯGe6ֽgpZPyz{_Un5Ԓ3qXnLRdV<Ѷ0-h6Z͖6 7l6Q~ZI&/'wf4BVQB49sA/*'U\Z#bh-\gh?>uF>#)HRXrc+k8*U;d~k̈y-J[* ;3_~Wx z!tQ N;$ W:G}pUU+H~YşjuOŸH$| @?.$S?1p+xBO Q N6ŋ9뛣 !nk;'?_zu'C>\#\W?^9:NDXl.\#Q| ;@xF"H"H$9UHA^\QR-D$deVffOW*M7oIN((ݠt X5[i^/A pQX;ݝ+.1XO _VY6IDŽGZI$s:h'V/hIѤh0{$ |()OH!"z*Gpc" 7x@m# XRd{D"QD"HTd%=ס"wP hӓɻr EB|4q髄- N9z} 蹀3LKJUF.LmVy1 :}ư1#}rf}Stsd/x0b vjѤҞUDj.F))"}tKsґB \tk~f/݋;O%4ʄa0~UST^7cm7 ˕Tܴbi3;"sQ@)!AHxIҠZ)3PQ<90PįH$9% mW$xgȻ 7\ vxǍ\x7U@Nx ju"3wXo`"*x!4L>]}|^?>Nd} g@E@:~D"D"HtD^ 珆"o8"vv1599jfw[@ `]eW}WL7a >>ZXR>[l`7E6uƖ_R@1ꓹVtHӔZsM?;q`!wl>dWȩkOOv;%Fr0\ArZʛ#H'p~I5͏`iŋB/Auj! ~\"~^Pged!<:SZN YX[{UD"u'Ot?|> P-p V]x8kkq+wL( V]I6#:;67\N"7@>('dܬȸǀ:D" DP$D^Ԁ@^$jCRaf%N0=1?A` ab}B'-MNz A|nku^E/X/[i_̭gdGݦh$ ACw?DNejy'/qni9^HآNP;oT{}*af}44@֢X@Peǐwn!ĤҖ/sH:Qğ~@Sƻ2DNK*D"H$Baz`#GCOU S V"d/?rpb 6< r0QӍ-m-^orۻ%C)$L+bz k#-g׵_83 gíVHDp];wX5ffzQl OZƃ[@Uѷp%7y~ڙ;3+C%EmCnEPABa6B'}8]=9%QTk꼬0w?KhpvDB5 ZEaGVfhx%b u(6{i>qD"߸=D t] .T $>ŹXW?^9'&f\;O Nt1g;>1 ՛+iU{`d"|"Uw]y(ލH$@H$XX:`xBį;BmGLB-o-⷇ :WGuiv7?)Sl@1w$h]&>ٽ-۬'nvvnVK$}l1O?HpwnFT9;6:Od=7Pӂlbm(8GĔ/à)—}WfsD<,b/i_۾Z[B։4Wrj7*e-Z¥+IҾ=Jnϱ)@7~G"i']'@ T [A_@u XT{`0 w"ǫH=8M| hxR<64HOrHӗ!H;ޅH$H$D^l%@'PNH^v{SLUn f T%.r ̶q*jvK秤|K#b@?z͟G_CN۬jI2P%^F8cʻס ξz!޹azڡΨfj%F%7V) DJH)ܙ+;nj`7կKDiРگv^]T ub ~ G΂8לJM3Z}.şr WM܌ǏH$9E.Qc?$ A,BǽQ rK-eZ/GqlMUDV^Ĕ(/Emv#oT'1/?H&ƻ/DNo!"H$ʵ\C{ 4JւHhqhQY R0\Iִ ^ ( KjA]ۯK65>M;#wlxhˣ{sթ,Iv &0iLK/^s|9_GRk0o$'i/`g61-'Qd)`)ㅡ@TXv~T tY^"xD`#ٝ!tf ME*T;eVOY~eH9F'<;',䖅OnD;5 0E՜덞l6Ax_}$/^Kch>&ѝ=vOl^}ےRh~jnew9wodQv_2-@B|M+U:\9|#ڡ {+eμ-۬N\7aD"/:\AޫhADQv8Bp `xPU) ,ΉD>?-ZlAĔ< E0(K#TR@@e\?ւޢ'O9S?St1PA֕'ߍ9"HE%}.^H$D3s ?r]()G ]Dj-y&^ '!#gzM-dYZD"}5=Ef81$U+N'?8=NMlf}ZczQvw 9j\|b-LMS'Eǧ66+Fh@·}![ko6 (YE3q"L33nHC"cDR8P A w_OihJҴ2 -^]1'Gu$xih x`$PpsyEqZ3ʸa(97NkWAVs$(ށ4U2p9bПy+PSl H87<H$P$D"/erDFu t c(3Loczb" T;, E`ouBݬO[FlKסNQ92ٱG~'']R;1b,x[ZS4͸KxЧDj %U[~mzف#Rh <*{йɌ+NW~SdO,C@ X:\b=ADºҫRLBf_ ˳ B ?}).ќ_ͯ eP΋0?#H%#WqO-8Nt~ x[hq@9y$cs"x~ k-UA"ZyZP 4lٵDMCL<杳S 7bJ}"Ȅr< cXXXHXЬރ'- Rwӳ_˚gΖ|S[}**sRRƋՃj4KX~KI«7Sc\y ?#H(i@ W|\1`ǀ3ϣ$FrpbaT7>xWxPjPCE\Kq,;T}zX[Ĩ_Ƈl"HŇǫD"sc {Bux8oW_Yq]L(,J:j?ÿ@Y1nʰUVn-2Wb4굌5իL{sS1}mǷZߦO* 5%\x!I惓y=ۿ-l?P(A|^_Zdv&@X|KHoclbvrm}ŏ#*!aSxbDc ɚH$4;ׂ7KnZ;@Y|2\`'ksp0pvIPjGЫ v G"@R-˅v; Hq9»e%a ps"HE-EP$D"'s\ Ve lAɱ@x2*BGB\aޓPR@)GKv~<̲LFﻁҡ0 ՙcirT7sӓOCgl޾c@{Ꜿs /0s㢋/ďl ex=v[rxS^p0;s_)QG6,B2[]kO|N߯ގ]/:?wE|?V:.hd l`C|gi8=`:~F"Hċ@wl‡ $ͼY s*~)lX\;W@6P^UaQ}D> P^`t&2 YpàŻ'DD"HIA^P rxQjVQ;6&Ϊn%P/4y¾Uݫ缳;XOW$Iuj28А /fYlLwS,۴c|n}|Zg2EeWpYgs:2-;G@-J rE&YdۡCGRBg`n1}zKP)U+S޹U ~]gu=V]LJ˝ =4\pF}wc6D""w 3ہ>7uetׁOq1g9  `q/B6:xțX@F(0=wS {7@"y?"D".D"3\SM𺌯/GY\@v(L+Z/(U|?@ ^5iIJ:clqIըS,7N8 cΓLeO?x=7哓 Ⱥgzt{3;lK"`dmpk;3TZy?,(#%X΍fE*h?g(įbU20g9"i, L8Sc5D"(-}k*|6d(ȗ@?tD5`'|dTTt =GvTu b Sc` tp7x s uI@oH$y _UD"HxU0RnWF 9k弻#  'yVpSR\=Oް;?@4[S#j)jY=20,YfAUt{؁ /0w-tH'gx ׏ ^Iw_1݂5y ?KBdϐPU (_{+K+x5&i͚ǣTK7, <<~F"H[0`'0uavc"OxN 6?HMQaO7/]$D(D"ȉE Wa$HS⍒1'RrviOoebl(DmѲZ1&*@QD̞[Vk__?}}B9<3ZFMkYV&]8ߴ0 7x;wn_ 33= |oȴ_hGi  GGmrC9WnP) ~Y,q_Gٚ04js+<NTuV.`"~|F"H;;xj0mA<bA hP rǀx5O)Rz@.#Ab  D"QD"H$rr\ \~t-T&tbmLOjRć}s @x*@]y؉kꭁhVt'K:,iQ2m=|B>煨^@Dk^j4t{PJ6VrVP#Cf*Lyه/4M)P 0{[/Nʡ&0\Vb0DNpp]2D"!VP";jA+!"ZA @Wp ^ ?O E\(XAR@灿v+D"/y *D"H$2 6P6'!O0F".UEpuyTD߭.P*#B^5sLquҙ#UtqI^V몕ng 3o|.Ng:-w~+^]?B >H*3\ = 8&dl+}*/Dv+ݹvQ" 9ppp30Bօx{ "Z WeF"H@(E"H$"qT A_VUvZ*^ ]+t?P_JBgd GGQRXa.gho"6ے*vCYШ։2W½p,\b$Ik=>ax7ىD"q?׹po86n~Jw I$ DĠv PDC鮻E{DP"1D"D"ȩ.Ah@t@ zffj+G 3hؽ"[X=l.EN,;F}ogD;͹3Rճ&zMn^24M&Vn1FV;vl!28; @QZfe,akGR;F;jfx9Q9 Vv]R'""*d! U+C5YN>V'OuY';˷lwގďH$96E@aWo$Rp: wr{#Џ{Gצ]:zǀTs{2px5s 1R6R-HF+jɱ̀uH$ 96x'cŮI8!߃Z1nV[Ay(xi W\ i&77D"H"H$9}\ 2RzP__A)#n{##<]>"rו;)ʍh]9csٲV%nc#Ip/xQ;wngrr[EN8h,_gm΋#6c 1Nfx!߽݌9xF7N:~# /hyLRkӿ)K8aսpPh0oaEאfS)ބN>df{LhD"q?W \ıq'<QlZ9p^=3t? 8BгA6}Ĕmny& H$P$D"/D^ګz)eLOne|t-!HT•\ѻEJ=sxՅFLcdt2fǎgEsMvTc}[sZ<<ȡCN=^M .bi6X?D$h_bΚ1ߕdK+"h3&Kt{AZ_yx/]COC$iMꞕ{y)Ε+wFjP!?2R)԰||W!Z!\#! ƏH$~-M n'Mr2ktț]Ma@E"8|(9 dvArݲd `-j8SvBqS0jA? x'D"HD MtE"H$rBYN@F!m|:,Gɑ}yQ<-h);K@ѭ_9xjjq/#ISUe߾=<:8{\v~b^V5EyUO$6&IDjYN:d.3fpkG _xsz9ZRowoxfV#"WCs&.]=c@3ٜoU9%+` &2kޞj\ H$9GX)'= ]䕠 j` FADQ )n{5Zߦѕ@FFm+QuR^i*AۯW#np0 l@(!W&oD".D"\0!@j9A%=###Ec@5}BP8ANH.+xc hpɥW^'4ڶm[Xh1/@Uu7Wjd7EP2ɣ-%"C,MjQj =PC.<ʲ33ܶz,]Ii#ޒhn:l!b>^PUKrKj_?pu+K?P\r%+Wfs'(W1SWsLŏH$s ][9ENـꫀgsIPLA"Jxg8"qU "v@ em]xNw|w"|zcD"tC H$~dASRΕ Ը\[*vLOQ@  YPK?lY٢,Kyk3h`ٶU|j\r=ЉPmCwNJNz9o%ə/"CYY%Ӛ<5Pm@v&"9KW<8Q9'v{ەÇs:;[-p;?o=I뤔Bz'B4EV (.YrS.zT Ć*oR$DCY Kptϩ6]/ A...XĆ2 JGm9I6h+psƭ' ~jc"p/pp[E'D"QD"H$2p= \@"AČJx@zE~ѝ;Z|7d0[kL.id5[+kG nٵkhZ'U-ܼ[;N== @fK_b6zPeddYx#'m˲5t Yp{ƚ3ҡz<^@ Gww}m?mӮZ"| @ DK~SZOG+Bg߇pV+80qwbrH$S~ G'w4ELqbZmz{z( ġ ^qw|_Ə SH$ P$D"/VXY*7/ xing6tUBQȇ(:NklXiS-!BBTd @62sLdNk>ƍ9胇??!DT蹥j\<=vuǀn=}ٟ-.^(OkRyD"qyW:<Ƌ3X@ @ )ڟL՟7l+W {vik?aY*dBWC t#$'8x1!碂vums%(fFDϓլ\ p;̬}$~|bu] ZΜXK6{«&0|+dQMu~u'`F|تED9?|hUՃ"""HDDDN `;3EGG\rP->b{ͭMxV pp> %ZZ RϝK{ߨ9D~O> zM:3G~s`>ĉdD}K6Cb2LomcsU}׹Ʃ!]_qlh}6`B9gp\,G[ԃ"""HDDDNV&0Y@%#9_bAŦet/âd D__3Ռ3R8pf$9E JcXAĞѱDKў=>1㗌30G[KX\]HNIV&ug[*;Fί.G ~瀭[m0 XMEDDD<_ǀabO8 ?3 CKt'0';,pa M@ɱ`jfR ;v< >a^" ˆdfOG9,9g '5H876G%R \l{""B'.r`19`\(""" DDD䅴6صAz$W2K u0\: p`p@SGrq03cxr .YLI+$UaӼ9MMyԖ[o[7Q)^0zf #c-u,]5La G*&UgW= tBDDH^ ltQCU'Ճ"""HDDDNm}fSOrϺYH~44 13\]Ͻ'~Б,=0^*87i+IcF̱C|J,K-Yzk( (;7>Go߲kή_D07&D3 !C+]pL3' RSoYy^V^_ƨb˦KQ ]`=ڞ A=(""" DDDt)fXE 'CdR4YNϼ!ၙBBOȎp}[=q#n޺u_{tS uwrj,9GC¥8,`cGh/\ٗ ÀSĽ;Omr߽w޵a層oӆ6m߳}F1_-כP,` ?!I]ͫ$\};B1Sk-|xhz?|tj> ,*E92ۈso\<0L!z6/ l'8I88GRܘ?q 7~fptvv[1Y hQCw/9T *mmjOc-a[0A!ſӮ&Zu6'}S&`3PK+/߇zdlp1`>'濁Qfl̎A1玚ktG{g~Y:rX]]5xLimk'8UTʖ;ji{90 st?[Z;~0N#by3瘯hnca;Oëg)ue( (IWP۫ =.DD^V7מ`8XᏈ(>W݆Q8Yxf KXQ 3Y3 ɍzÓmY*5n` MMHu*ers q,sZ{C3TsΕ7{W*UQ-ͣd-Þz0Z@{kQDb$3 BD%:x- ܬ-'"""$86a0ۏOuAzd|]rX&7P>pxzdȞ7(dzsFQEF`; 0$BddmF6Nޑ(ET OdžȋZ IJgg}"""HDDD^l]s`ŇHHd~@Q,@قbi@XZS{o\zN<\WW.f ֜MS 9<1e 5Y]p t8 ;zi\HKN%$5 L#KYT10E ?X̉Y |xP]'"""ZNDDD^Ź]s Hpd̈*@ɂD)RG|y&UUG B<&R*$5ɱ!:zV Rn jm}`s5spuh,~\v y)qݘ]x೛8T_oc8Ј8# l6Ǣ́YN_SugXхᰴ@vl.`>WuԵGUm?qj@g(S]x&"2kD3~. >Q$"""/UZ;}cK3s:߮ ,L:R444ppB%n-d2]…/t.зQ_HPes)R8pKj| \;NDdV 2T瀯Ci,"""HDDD^FC}ZSeO {e rPLIZbc#n I0*u3g0l8| 䋥?  Lpf<:oQR`-3R`:3(?sKݣqJF`ofױP($uwvSuwկއdwk]'%uVeYxHTcQk`3 ,8Bmo~|h]'"r-~ßǁ?^GDDD@.6R^$Ginn#UqSV8|,q 3O.7GuYRaீPw)W.ocɇ~4yOM[.o.PQQE4@"""rF3$7-8ځಖ  pfj{\PDDDE """2 s71ܽ2LR9G2av+43.o hα@eB+|#3@ ÿ_~$͝/ !"rkOA`3ONDDD=pF᫘יXBҕաdZږڂL`\)鉎E%_wչE9ks.` 8fqX=rAQ 8WΧ\v#\e"7609{|`h2:U-4a$jLU,ج|x`$s4[*swc#OYu=vt+^u_~&\G} "#2+~ @WnKoP#""" DDDDYE_+?][y-x|&=.Ar`0\(&+Z s,7fa4c0g'v^m/wD7A4"N*OD. N= \ T>Q 8G_w]o7Yi|{-jĪ]+pEwTYRHdnnth|SX'^sWڤd9"? Fb#A[t&7V L8Lڊl[IME6w:Y&`0yOIp """HDDD^o:]7<^˫,y.k9/FCle, |v#g\F 6B/m 2Ae&ÏϋKGQ74S m#})=HD#^ Y LJQ 89wce6h9 EܲKI#xρE)9R9)~ ~GDDDDƀ1Y8 }`b@g``V-w?ډ쓓ZlӤrw̍8oYR@T3i3p̜H3[6t15k&.d끧kT>9I,p=%`DQ 89 r,yn8{qǯJ`S---y WR2Y\me'-,yѼ@FXL}kj.96;ۀiQDr~|t^4 w|d39m'/p4E=4e\mŸ/""""PDDD?V]Q`mCARZ(v\=13ʰ *VpZL'6eaDE㳭*N$_DDDD / t۫RM^ew9}BsUmnwB WQQ4TK'Km&]氈Kx%F Cù6mqᴥ7O,"/Ypqtvrн[A\tu B 5Mr<ޜsϹWN%M ELg9oF 2,^Äo|(RE Bđ$TcJʼnў#Zyb@s}\TqDEe,y :*짔m"GsD2O;Z/ !0_*gx= 14<_D,9#0ѼropMcnuiQhtB)¨ Ų,:R6˲,o]m k4Z;T*[s oͺ BTu 1ٯ:k&+vV.s}pk{BQ09႗rI{ﶺSR)'!vioWul C 8a:O@}9¹ );]R()cP3֦vGxr>%pnemβ4s4߁?KHQ\Xj[EQ m*z.$z,* FjMKKDQL• P 5" ŷ:fwa?sV?{ws5"?@gg?uR[ʪf}Q"""p"""cfOsl130O38>1>07\kJPDaP|ed@Ǥ5Ϲ(//;yظ>V@"""t ^6qx0`8f?<&1PH,2}@6_,J EdVW. X0P l1pf&f VBq{wӹu3M/ODDDOČ91#* f@9  8t/VOD<>E@@%"la+99۔h/$GVuF\/󰨂<˞}8m/!a^z"B,M4x4_2]Z(Ġ6do\NO+,w~&c=444լY3q'nB!'BIvmL fϞt0TYYKr;w*M466멢!Сxx<ĉ2}$ kMeg[Bii%=p:|]F{^{1ІVV 5d ^~ g}2`9` 1 [1N8 )뛖V7ߓHDD0ƍüobZ?0PL%oB!@B!N[[V%VŅ W(/<}ّǙb͖R (HJuK**jX=F^^iR8SUUCT h2czmTsF1DH#:ƹɀ}B 34 O*2XŸlgҥ yjp\fr 㽆 s?%B!B!Do555sz.q2j5nfiztnft\_#3#sPBÇ3o4l^/'5T;s3|-Z!B!P7KI znZGZE 1 rj5mfl`*8~D̵4]R(ΦCkLZI N0A٦Т6R?S>"a@=c{!cw99`oBEEĕꤖZV=˖9ɱiܹԌK'5\!}G M$Wǟ5&v?@Z6 ^}m h<=vdkA9ܽ9?i0/qu@i@sm_ϥ•Ŵm[_hVtFÙ3;|"BI MbzU,!G {?wH+TՏӧϓFٸq @YfֹNmj h=~!858h煜U NzaT( 1T(*ğ@F~RXT*"MdYI\ܷ?@=7toBT|%M"NPjji@Aცr;:dlbbΓڿ?@*djj V9:n2y\\dj9ʏMƊɳnѶ\$8 DHh@"ƍHqru^Z$%<|W%iklzUC6z/"RdbySii9)@iؑ\G_LNsYJ_z&c7 |xSAαŝq J u@^ ڿT7Lo@32kIT)Mr[jWTEj[}uYRR $ox88'<ig 8>gs$+klQHy.]XLsp6|]%6%B dz2KK˺cɢIsQ@7DZB4..L NMMg^ߥR qXsPB!Bzvj\m w E{I$n~rzYH}n(rBP E8_y蒢׳9\~N]ncq.>>gN@Z]S*}㨺H1mv;L|h h4̏fQr+6?#t63c;^1sdNB)"hd~vF B!BGgf^`C Fkx++otn$KK_VVf--Wsn(7~`Ŧ8NeIk;oߵ( ËA&7B8K] -$RmN-q,"88-ġP4sI}[-9 'A(BT!mp &`*StMV_/VWr'޲𞼵+|j&)ƔS~bq@O*UC 'Ӽ<ςa(5 Ͽxc(w+s$B!En#nwN/v;3!}t-6v R|`_D^(g纳|̭%tdwBauI' nVދ5\ 8O7'0qoy?D @bﵸy V+ĀԽDoT&]y Gbυk;΃EIB!B @kg 7&-΋gV[p˫ VX @Dt3y9;h|)l^pߘ8FQ&nl챫ֈڷPJĤa&ķ&{\urʭw|hbg(>_Κ NOUժgU:{.ZXE [N]8gk>_)!6 JBP"rwﵱtcvx4(>G:MlNpZl@BElb62Brq|_E)P@ o-AH<Ñ#!{D=\@Ⱥhժ32/*Of'scesTY>S:{ Zlێ{p@&Xkbm2H==_)U9bdCHKԩsd%9QT1[p@I1#ߥJ!B!D_@xPuL9sEo"l|'m!b2ʙa!<X8}j)[@yƒ17.!(wI rS_98z$$sI\sD=3B# 2lx3t,гeʔxqV^F?Kh}gҥzaGYKUz1b&I@7-%^zM2Y:kMuo^U&a+UDOO!\*'H.g)J6=R&99X7S={^/=(!B!$f JAheE4пuxPg Ǜ!^h^i$lPvvD>|t(3w/P6kJKcI^,1Dn ~@bU9ć9z@ zZS>}V#fѢL&C裘W$6}} ii+WFTd1<~rp֬Y pIsΜjÆf6Pr@Xb=RhL˗(Y?vE)(]鳑,-ٽ{d2SYσOrTRFf%}(%,]-[QA0D~QB!BHY"@k7^8!q:(q^> ܯiqĩP 3͍?y8dh%GM%pHp5$.Xk$~h]YݺCV%̗~wҤ4XbKw͞4X8i$}p_?(گP~]UZjT| \K ʂI1dHy0ȃ-LZ}))̕j۶jmjٲ> Qtؐ>:v]v9]ңAWQRnݺ/C |5o"5hЈ=ܮ]5c%P OeFIQITv(l1}3Zj-_ֻ|sn@2Vb>IEHe"6@B!x 0a'F-s@S$(#[jUaSBk` P7kK,f~^D5yn'"KbĽ[+ F!B!MRh.$4jdw9xP رLB̞yRjl7eGG0xG|B"B!P&P:ّ@ #'œSE"yJg܅~>#Gh\R>9)G@wi`> ),G9z`ķoD3"B!P޹VY< 0p7duܕMq)qR,0&Dc :Ai:J`Pƭ-]=>I͛J8bk<;S19*Ǎh(aG5I==lxsc$kdϨw?&mjZ%jz+Ӄj^s xzT`z D$(q= 6+J6 @=$E`+|z|cDvJO&"G+8(zo7RSU4OM`}5VKQóL#!"@! @4}@\"<٪JЈtAQU_FFWZq# [ [A-ZdȑIATӏԶL*y`o*6az D$(SsE}~III$&,8q'ٞ>. q".թB23-;2wIO7k|U-ӧE[h9? I rxId#i"!" l̒?f$;{ ZuFX Y+hY?)1t˗ ޒiQIÏ'>4ʱY.}g4}q&##KO@PQhnP=j&缦:\W kjGBD@ o-aࠑa<;j޽O|Pf,t8JU,xb+f}aN.ٺM7_ڹs<& jo*.S0;LU<ݢ@H0pEA͛e@ @6Hw]!v핢3ۢ>wGV_fŊ52c =Fǎ}3E5k]2jp @v^uU4wk2xR[ m*((Cw?w!T%R'gT:S& sHMU`s9S!AtFo/UXMRk3|*՚L@gM\23 ?2ܭ.Ujw-xPKv ƍK~1qy~lfC؉Pw&5RsU})^d%;;G\E|@[_UQHS-[)gϞ*?_/.[]ٙD툱&T*MS">zObZ$\T*;-3jH*nt`db3N~i(>K0I2,cDzԛ.RŴ/XgqSz7~JO; $8~t#T6Sc`/Os{-p`dƐ-i0Ӗ'T~՝HVF;RBI*H[$h!3OT4^JK=Md*v% @ @,!hCH㩭CGڛΧRѥVZ$BTq h6>Lw,hcz[RK1`PiO{h*ҚyxwR ,!hu:&R\Ik[L=:\YX*}O'Ƨ&S_ZJ9;;ҩi>g| ۦ|ip#ΧRѥfZy s}HigjsGuPL_ҮdcEڗ.RхԛBW?mNRى~g#uT*;\{>ГT*{/4Jei0;C[*^MSW:Je#;5<A7T*NcT4N' 0CЃTfHϤSʾL۝&@s A77T6v'мCh*Wh"L@ ARGF'К#P{z6IҎdZZ:" _IENDB`icingadb-web-1.4.0/doc/res/icingadb-dashboard.png000066400000000000000000007404141516124654200216000ustar00rootroot00000000000000PNG  IHDR?_"zTXtRaw profile type exifxڥWv$9E^A,,C٩z2A0p6]J/'E?|~?/?w^E~>h5| _t~1E<ۍ>I#uu?_7i[y |~w'_m9)ƓBS Eߐ2xmj. -N?tFt5׋~ʏWe~luI-׿OUqovߜȅPI}O#Z[QՋR~zv󾮰bʋWL-"{dQÍ5fL'~%v{Z;pi ,._OpZ!ŒԇO~cT2T{5AS"-#AzN\h|7$hc0!|@   =3 d)rӢ[jxFsLWMOdlO͍,YjͺJ.VJE8jUjjkcOko1x΃w.cƙffuXVYuq ~nq¡N>vʩ~ƥnr7_nG־2 돬Zo'0X$PQ9-9HWXd|Bs'+o_9fϼ-k[4^>]zh_Ψ:w-qj5{ itg2ּ%vS7n9 "X%jM'X م[nR6ֹN!.UrkV.0R 'eWOGAd(};weaP$@KNB펲O'|no\dca|3~sKŔ%"ɣZZf=]JkS}kvL' =Ma/J*:pȳo6.t_0C)ӈl1f ~'VX$ZlAl35*UwnYd{idwenV׬Qct[7KcT'[suۤS=$Mz;=;,0ϼ/˷}wi$.T7nn>I^C8_>3CנBOsn=#N)JNt(ܫwzj13>s~q@|3cB0f0v䰝3 n2Iӌ4 \({r[ELʸ7Zqo!sOoQDw[hZ$[ݱS[Ҕ0S[y_r9d*[#f~A_#Dk"L18x@\РLޛ׈t$y^dAEMa]I-fhD›bVu!,2x _;|Cr!s"'`RѵJtD\G&"9P E؜sOj2+rX &\3FD|hϝʵ\T`bJ3" Vq2a؍J{,@x/'kahUh';oө_4rJMY3IjIQi# MVh%膂[:5PO`] {&#vPYA)L(QГT$f5 x4\8uYlJy 1SJVŝœN(ىċHm[=Hdμ'qpz8:&+ M;[Epw$MVCۉ\y']E2B2SsȎ\xN^ m0=""L)]Tz; ۸؎I7JT Rw> 4%d)It*{5Ө: \Df#tpi3,]E^F,H4y^SLLLݩ#!nm>,R4GVuKYf- N6`AْLWQ߈ tX';]p;c?xذqh0ǣ*%€_7 "_lKH_⋘K&` dh1⽗%%zBAvGژ y(T?‹"=D cͣA;D*wosGGv.!s7buaJf IvbF]~ ;-H6xsנ)H×].N lᧀN *=A &ȌB`l YM@'X H wv\:Jiw!dxwxxH5@Ђ]ԑ J@s RA[(M uHWWh1NfKZ' S%t)t |p!:&+]V g!8;BI1̌4N4Ԡ B5fV' |+J+I` 6I< vI21"A'B@ka&!I ĜXNW. v 2J`k,`?%`\3PBY"*| U6*QRl‰*m2Z"a<`:u4 *| 4SFѲ7t~oѺzG]]x9=htGJg!+!@)_P_Md9HqJbn A-s!P0Zm%4 =l_ፊ.m~VA4 XLbn@6w=V185JWx6)`BΘ‡eVz+hJaMp [%^CCe Tq3(~:BLDe63rF 2j.iB۹ +N&FYx6dBI5fq x-TQPFZsm:0'#č%F ExKU̘%j+Mbs%hhB4hڷ]1•DDX|D LPMn). Eۈe9Y wt,y#`4j&%^Pf$aA# .I7 Q 1g"de}tkpq].fB&!HciClRAk3BA4uS+XʰxS\ |ȉdT؃V6d5TO40riɆ&B''H~hZ!:ZԂL&pii ݦY<mDžft *qF|4H|FԂ*7Xii=NI0 Hg uPѲe!:xnj3;f7*~Dxciw(֯HFK]Pky =2P؊ӥO;z"-'BZ󧣓tc↠^5MH:5HGq-Ԗ(^Uh'6@h =J2xe-2H`КJγ Dx15̔墇YE_ z&ADŃ\m DtVӀ?7cjNP)$5mB 8Vä.`1p], K6l"̌[k'/kϫD͂:s)6Q{=ʍ"CB'Fp2P;7 t<ħV5tv+NKAycaCA;'_yE(2Q~18HPJ+]I0V(B!Pq(ڠzM;=:P2!dA+ sA.4lnfGT# BH >oDIف,;],DZOKoe( \( 3mL+a,ԙqIZtfQ7 ˋOP?hr?tTJP>,d18(*XL#7|k-jJ5 jZn0 x&eH_{^(Ś>TDmΥwMVd!#EWZҡE/"ԸY!2$XB}jDD.KMׂvBG46hchUcp|a/1ybwxHDG[n@diMP񢯵s &SD-S+Z]%ez-P9RQ@چVՍp'r. "=( Z\I`kBRmkiN P(Dž/R#褄@Ա QMƝ>9:Z3`UCz lKSciiM c|Mi0+-0ٱFH΍ C?@9oQt:> ]%NdES[eB-I|AЧ~Ė qh-v$2ytTtUjGg0$|S'tB\h U @GD5$v)4|Z 8nX~Ny(&b3t;{ϲVlUt0GHKK_[3ryh@b+ F tUΌ~dE,P@0on&2rnХڳCWIaߤ|i8ͧ޹M/ H 1->P{Qx~St!s St Lwe)΁5`bIтDs*.L#o`I kk"Oi_,JsfәcRT \fV:_6M~77@݌U~7 N!2tHw fkK=ELMOb:cop*=kY|B; [\:-)HN'3lTuJ:ǻ6lK*4PL ⦵wTPm? U^xg4 ֚z# A;/~"u7DmH_XHB: OI;4 G ^X@ DŮȏ74vS,!vc\r4EO?P.baHAf1 ؖ8f~Z'{C:s{lCKsbjx殐}Oԛ|`ZRsW~:=x2zW̐kkb ň_69عB~q9C[^uѩ2{9 /,Tu|J9%J F[AϯcBFYfo3f'څ {}_ځ0嫕E~Y* R#v+څA *Rn%H5!&cpVEzL',EoղCf:D+PNHnoJ P:h/ !=VP5)!ƹTJrgRDtb 7Bf L5+@P9h c4Q8('5ڐl[1T:PR{) 7vNFv TNo>$g\N MǚĆXHxPq/Qpa谁N ~Bθ+Izĵ2J^j瀥Tgƍ7^tf oLǬkYZ`VHu'svN $e"taFsz4nM]KM JO)uAksS 7a+ bښdmPҖ%' ZiaU}zJ:ݵu,y Lg1$v"BS|(h'Vg'b)hi3B+%O ,Pt#Vh%Oe i$p$ +Ƌ%އ4{m:BB8.$&"1fPQ*+[kuU9W.jLtչo& ln4u8&:6OȰM48Gx:Wrurr!@:,ji:E5O:uJпP'"5d0I|YN+%yfNϬ?'Ka .D: OdžcvfNg!sJ턁p' 4GGyݤI|* r:~*qMڧ1_9iT_ZKCYZ-ahe=Ą')e[۴|{|1'⤊Dć~L+`( 9d69.5Lۂ%DG5`2NIO6MO}h]rpG-R^Kշ O-4Q07jzH +1в-[oʐ *u qEW :?Jb J8n"8d$_>fM{xWh($}1"@.&4LCtT@";+=izǛ7-1>̬t_$@g%"r1a^0| -sV>}*/]~&*L,U슰[x'wLP)V׎*Cb4:ޑhNo`7Oo~ft'xt o\[g|;.,*}FT/C$d t4WAkd?As#'i*Z@CYK`u0z a0>.L+KU<=9N hjmH$MUPwʰn!BZwE|ub-YhӞyRkn `6҃H|t舞/9~:ߥOa):d,[[f T9`yQ=P-h,'DbS@7;-:אNӴsn }U ٞݙq*)1֦d]Bt+EruK< 9FL!h 𵌊_q~Ö%P85R03ü;Rֿ:ƻڂVds;\&hh1SESh5 Ƭd / ]٠SDq,_zbwxs1 }:;ۢ~:)O[x[}HV'_QaZCw͏z iTXtXML:com.adobe.xmp iCCPICC profile(}=HPOS"U;8dN-8jP! :GhҐ8 .κ: x_Rh8}B4c tL'b6"^х>*1YƬ$໾]U" 3Lxxj68GXIVωc& Hu7E3bfsbJOGUMBcg\e9 y}yTHb(#Ul quR,<rrmch]?*L{ q>F.Ш94N3p:0IzEm⺥){0dȦJA*Pr-г<R7!0Zk>n;~mr4 }bKGD pHYs  tIME49 IDATxwXW.U* QQT{aEc4t%F| [lhIDdfTМy|dfgg=33{; Κtxwhh^𺆊,%\c%B!BQ񥝏 !B!BQPCѷ3elHI!B!BߘR̗$B!B!ȕJaciՎVV/\FLB!B!2KmSd\$B!B!¦)I[=dB!B!ګ<M[@en^JI!B!?^qiܼ)&&/I2 !!Jc,L$>#!M#B!^ӆ#Q'^S)DA >_7smǔ:(IM|'.ְme0et:)bqdukjE6ͭ/Ş?Vq|VW#i mD=t(ݞױkr- D }lsNV|6`,^\.d;0ɸY;w/`|7%t$mظe.DϦcg{@XtgQ [9F0?o{Tb/ݫB.'>eao6O_{QQP||-v¯ޣV7o 5zLf-ش1lubhGټc[Mj9ɦo:l`fWHiOoFi?!պ|BȖ+wHQx;Y߁cjr믙<7Cs`#Qn?6o)3MoW1= d׺ tUej=Y ^#?cҎT5ƱhVm_-0ͯ2coX;c_r(j?mWnsIR׹Iaukm)ِO?ɗ_51{d%҉(产Wڕ ~`E/KKAYlsͰTH-(V_zc[~NFp-ΔA13&Nj!B<UaʯYfM7 &I2 Q@\qq69J8<>WK$}W|=.Y :}OtjyQz ,2:_iѴ#V;%G\SF{+*EqZ4g~4%V=0re܌n[X<󝹘 ~t7UPSSnQh9t>=W'YWUNߖh1r7IpV<أ|w^hif?1fҩTw\k4l r:ԺvzhX1# dP;GQ>3R\| " 1me>tlAs6#h}!\WR\_h41kOش&< Ã|e(w3l.I,4^߱?GGYN RzhhY1e):L3MvM\8&0Oo:֜v&Ǚ&⧙ܔֶrK B.jܑek3w|4jZKd%NSK t?%WϞ۹6,߈fX"9x>tmi9^I -k"RVM6,D5IX=2#غj?4$]9٘b{vz:jT^[فa(ሕ*=A v_%1£5JոwwF97lՌ}xpD8Ұ}puk.8M:mH.'u晤TV}GãLjpu[Q?рzksmzI%=V hS,?.AJ*LȉBvw"cGf=hH0 Nr7)-e0|ٺ*0J=k5QepkU**1B!xG4j5a\={ʘLB?CD|t,j9J}6!PqWWf|?$*zMhxNN-1O꣉cBe^;Վ!rgI4T+jՏyZ|VWSJ24~Cđ-n9{# '~C%sC/)-$G) %:E;(XR Wxf>RzEea?;Q۾L{ZxnO-|ˣ?{kg+O0j9ǯ҄% p)?|zܾs;1sqܺP+YSx_1+ͪS36M㞺s;[4a2Tqu$‰o{_afC \Ih@ֆp:PP˅]h }$ H41G| LPBȍi9Ls[p󙬺*B!o)tՍZu)'❜ؾ} NN4jKt#Oɻ;өFIttQUjɄpJ$]&ji-!BuZ1cELJap3@O_0J9u^kP]պ g9J*żC?|4C6)Գ9bMM3T[҃q?|m>)]GMRzNt >%gy8F.楩g, xV_g=c\}jH|C dRٗgP=>LS:]a|4"cH qX9V~r yгuIt( '0(72I?H@mjZ* ;? =} s4Ca`I|^k`K'܋1+Qd$ӣ?BN0=9NY;|'::\u,к !Bסk}VmCwSqvu)-&M6mQ(^R"aa|T€GW/ݝ+YB~6;L)g?a1_`wu__߰0zCV^!dꇥ<~\؋3]<ԸjcVcaOɔ` 7(9#_՟)j|z< 2c$&o`<8J+̘Ā8oF /ŵ`캚=EcV}PIȂq;qG~|ߣ5(JgJ3N/˶} @`V쎁;]8rYQy{o#U|rW\/a1[I\yO:#rtN Xj OnWՓRAcF}+4** 9>+>\LeOR~(Ts!\6 5!ג]!-ӳO :uCj(E ²&)1 Kmqw&G;MoF5Tfcn j%o^t CP.F$&fFؖ3B)lhԭ=UBI|5oƪ׈)!B_VtD՟fhxg[2EDDٓLrD7#=t 7Pw iHHx&׊hosK8?9B!FS.TrRd&邝v^LLB!B![a{]h+IH 3?(3wndB!B!D7t/|ފR&wy݁ޛB!B!D7t/};Q+{N2T89U!4d[F,-U`B!B!D7#?:u[["dRLپ} NN]MDGQÇd޼r !B!H2, }hՎVVo}}lۻ)NN899CPPH[=۲sA9bB!BQ6Ɵ^)aUWM22֋WvU*S<=>)B!B!ȉJ1U6[XsSXw$%yzШ aatXLYe/&89UN?~IҤI@l=ॺ鉢@oF5Tf)zG\=/B!h)k1hڲ*swE:иq=OO.{ٖ;*;$<<@9ҏig7eV$$B!EЬUKTwoSa- qʔ)rUVᵴi3 ^ZUͳ/#gB!B!\E 7-allV7`ʤRpxxtqU+VzjV Zm͛![BUS B!P)m}hܸ*K2M4,:99Cp&+M,ݼycشi'$::o歷ᏹ)AAl޼W!B!Bֆ}iҤ1FFF8Uc2DFR9}=ѣ8v,x9I2&B!Bׯ}g#O xZ2$`ffBHƌB||B!B!WUOo?=% wٷhBttl{z0bMcټy/ ={KcggCXEs~*Je݆v{ټy!! T#ɲNeKFs+Knua˖h]!B!x8p߯=ח<.wo{ <9O40ed[F2e͚4j뺂BۗᵨTGRz߇z+,=z%<<2Ɂ5kfbo_:ec2CRr6mWogY^?.'B!qwO8AWWWVZ~ʂ-¿3Tl߾$dZlJk2̾..2NN>6䑧Kdm͚;^>YXمB!B3A?L3p* >!_x>8?ecYr6|7%,;78پ} XV'bb۲/wW!B!Ujלn|qpp`oei¯{c%{{Ț5=:ٳW::>M2m޼'ݻBX؅|w{~u''sT =A+XVwGƾ/ɥשvjBFs}Vٟ0d|NG5*_@2/cAEGDzy,-O @hIXM^^MPL_kK.!B!"'}ѱSlll$o@L2-Z~~Y} ֬ʥK^τ ә>}. igݔ_EBBRvvOOιs^ ިM2T"B!BF(Ϗ^(U *rI#%2ў?{k]aahf\ڛDhI7>/\!ˀO_$99E;lW8z"l'KJ!B!ȏ2lx~ZL$T)bݻѻ6n'ϲrF:vA`B-ڵۈٍL2ϛÇ].4oŔ)0p~ dڴkO;|xmtF;moRT8!B!b[ƚ&'x2ڵk-"W'gQ#5rq8 \h =AH,Fp!!t9?AA!vp&&MMPM8Mr !B!Qy2 Ї 144Et%Y=;ҋ[ =!2croGq/|-&&@ m3\&rgl79C"[X\n IDATLjRĻ]kvζ,DF!~\G7LMM=c;ZfbW2/9%7nͬ^ j_og¸Qa B&\͑/!xթU~(&#GO0e,:4l.OÃQZ}dĹ :9wA'xcAw$!'e&k^ ) te˖=YP+Wnɩ*vv;7niaa;7'lٲ'9pxEϊM`40a:}Exxd19 prJe샱# ZMZZ:+Ur hֈ~O/TXƿGbX||!xTffn #b =Ȗsʗ`az#?e244N͚Og=ʫP5kV.,dKYa] 7W~Ϛ~@ Ԩ^-&I焅]{1y.Ic077|g\|?ڵ<~3Ɨ>TttQ~mބu UykLݻtLϷJM0}7klBlln2sٖkXFӰY{4Q(XbZ8twI2 !x#|{+gMx6?H:V=]]vdԭ;ʧ=033|W\tM;z~JZr%dzVYvxԪY}W]]IO d/K_UesAI% uqq7G`ނeիKZzv-`Ą3gh9:K,_4tZeGȡǘ''$аi{m ͛6bܳ5[6֌3q%/ϿNiUñLjUHMM̙wFy+5q鈅 33Sz2|H=?l޺7À~hބrvD޾ + Ka_!f|Drrre+ލz0^mѭ3*aT[ɢ+̬۰pRNyYsyvם+˙5g7nƺ;Bd3atBGG'}ud(e&pŻ_ҴqC荽-ΰ}מ|\x9?:ileanè|c٢ٸ֭Üyy8~{2jG͚F29g~JG:S߽ WqDaLq®BLB'ܹ{FB`Y~#֭۬\i511fmRJѣR߽.ulН1};R|9 $*&}>iiiҬ'uj\;i$$3Q(̛=*۔/ga)mcg_MC! ! =8{3c?|g*$J("?v0q]s۲e[۲?|'3llLMե|9{~q.^ێ9luٱ{/Ib8#pwվ^:::4k @ZZNݓFTP!X*Fr`̩(JJEqQߞ׮XӪE<o#8}j:Ұ6V~FV4kjb 0t`_ޯ.M7dַ(JQW:y/3ֵ q.\$*#|e#[8FTP(رkY][]r©^/[^+8MB]%SJȿwXGj5εdoZۙtڵiF/J YDnlz|0fDQ!}5K~$.>C3n>6pz5LIJJ49dP_LMM;}&-}h‡Cÿg׌ˊxuIv=RJү_a]ʊݺmeTRNy?a߁C4gj5Gkt@tL +GNYm``OƣqZK4²$ӾMY>#daPa+m7:šJ% cσQ˰Bu+8}2'C9~h};a? w7/ɴnߕNS>?*33J*I@k{LWWzHlޘeJk= latZuVh9Ivcu};,XÌ2-[T磫jc}|d޽πc̔!0ң/>]Q4lgݸ1#P*m:Ѥ(SƆ-PҒC2Z_B6hڪaR!m1y?Mܽ@1(_ΎYs2v|EP0fPJ%~ꚯ,"144d>o\֥Lj'wر3#Fuj=\1zWu=xS{pL1z`zIK&! hŒ=S۩&ի9PTI5QCj8Vews$A@>$U12*ecO]mǮ4m̜=z7&))eS&l6o6;EWW02*x8kyֿKG/t6Gܸpzd$q' <7o_7:l޺GǑ4 _oA❒ʺ7#1o}Ƽ#GOг[g=y\HNI@_-}<|=uBQ8Tff>yQ5tqC`SMG(mOa؏044$%%%_ILJ2Z.X# p P͡2o;~ǹN-<x0hrїkJ@ՓViii8TLt>x+3!֭xpKll~L6\iX<C]ڽcGX svv0*? c~y8am]fYm;g]._칋HOO{Y+cF 5nw@FquqƱzU*WBy{*st۲e,dzuزm3fi]Dčٖͅ}\'$S^_GVm[7׷3( $/ru\-ߣawzu?抇k+ U*Z6Ҿ]*WUIܟcWM3y1nݺͭ[.O[ZX(j Y_6lwL~R,o>|][;Yoܼ9=?NNN֎9hzrr61!@ |w mYvoGp?\-'BA+ THЬiTP="p`g[QzgIFf>Z(?u;Q{MӾRu>>0{ٖr;UفKY'^q gX[gIWWN^k;[mOHf<73ffq=NS vdݴɧy>Gt'u2ܹ̓s6/|.y)?TK a6*W@ iۺ9*V`ܘܽ7B!^g>w">FGvږ͸&ZDT# zuiҸnuIIIe=ف3p^[2( mn߹K/\3I%,-9Tڦ7n^KYYM4e^-avK-x6pƺTFRɏfqNKf"Jr^Əz%Ks[ \eƊul۾Ktڶ}]:yQ|#{ g}w>|M2^w vmZP׹v,AKswN:xe\΢+8t&D>nTB>-Q3Fg^\j[VuXb--9{4s,pSuŦ_BAjr:#׬^=0}g5DjvӖܾsw湞]Gcabc/^=q6;wױ0*fȣGq* ˲E*/)ag؉qaOiY s z-{gtZ2fvccv\v޽agcGs@)y%55_7mgtiGf144@gí36֥XD2|r|4/;vaޔ(nk\r-eoݺJ_"+J0 -(og1O9~/z~GsU=qS٢vo7n؍ArUJ(^z֎؉?uVRSSwk~=.\`w*U@\\<Q !x__|9;UQr:bbbP׮E 7GpJ[[/ghzkVܽ*[7gEp mh))A8{] CebbcY|%^v!#a-1hme?0OLMgܽo12Oʽ.͜K%\|p_N>uߺu.=z\r 9sӾ͐S$1GbUr#^V߀nl%BaYY.M}hXR%B"gPF?ˏ,B(Ϗvbmm-/$B!(*7纺X品ɟm"A__~{,:&!B!hhߦ%_1{KIQh:7m۵Rz$B!EDl7ؼuG1 u335f޴l (IdL&!B!( )ӪUK% HK&!B!?Beʰiޢ9&&&HI!B!)Sڊиq#% ═$B!Ba[ƚhҤ1FFFJII!B!xUTDdʕBWzc !`mcO'8E__tRR%0Qffk$x7B!kB+;=ח7BLB!B!;©^=Wtu+xB!B!r׃:u$$ yB!B![G]zs]gttt$ PII!B!x4jOP*Q$HI!B!xKi՘]B"奓Lv_ڵ#=]B@WWG+ahh@TT4O_ܹ7…\!GB!B"}ftQKH+pIP0eXYY2~t.\FeaΜɌ7<MR[r~fx{7!%%+Wn(IOWB!Bn߂] E^LuںN&q>|̉gX+ Ys=66%QDEp)LL055E ws#wPvUj[@ݘkn\#GˆMږ"66Op]WQnLMz&G!B!Oo*V(o6hP7IOWB@W7cӗR~ \Oxx$5kV~ڔ(aŋ:uv>Zܹg\3R9?FÕ+7iЙҥͲe>jb=w*11q( *WcHOWs54 JvII)ܼy}}}9B!B]}킭C dJOOƊCZZ::: ʖFPuEF'2>66%tĤsRIm֭N*$$$JBBvDFJJ k R.ؔV&f14Z/㔼_I 쌋ao|k/B2xZ- !x:ZbTzLе0 QH8󀤳Q$!k7:wZ!zN2lfSLz"K''wdP8KoShΉgK 8xv%-re{lm FRR2ժ\22FI뢫KZZF+gjڧIbI/aR{ IDATK1lӣ /S'$'mb2@x,,,066&..j5zzzLTT$%8&]]=qB!zeMP/~y7ZRN R:)7$hBWJGG~~jGR2txw84gJf $((Xt^;/Ӷ-sb8Mka=X1ɣGSfeΜLriӦK)b`GIL;=]둸?NQ5 8u(UʒwHOOf!lBWScSfڔE=R̈U{KjR(5*c餤JDGGӬY3X鑔DrJ*L;<2U!ђU3?zsQh gU jY_ތ#xjBzoo/J*{gffV@En]uv֟ݭ(&vH03?FFPUWwy[9̉} D_G''+0` ?eԨl8Œ%ѣ%gЅ 7yޕӧm#..oh8t< ҳ/TPϣTs4 ݺ5Qjܹ͛oԩ+X[[гg sO܀ #ZB561Ӳe3 EdFY0@<gȲW_++K7FիWz@ٳg122L2-[+++nܸ;x/_$ D6^c͊c\t>Ҟ%YbgppfFgCN{ ҏy&N4~>>On+ }O+7j^7'v#y&w&:)Z0A39F'0(C9~]K]}!&1/sχ ^e=ʖ` ;oܰ+s;NFq31e,_^Ob1+̣WN_0vPNuߓؽ'̘0fGoҹobʗ{_*W⮟{t&'\8sUL)1\99s| ɒ߻.];+YӧT*^bүn?znd͎͞mۈdׯ]#6F@˗[sfojgOێcǶm̛=-7_rQRR2 gJFFF$%%\RRR߿#f}\~?cy"{+ٽ[BkٳWzؿl4:4?n;**Ya}B0{: 5ibT"H"( "))-[ͻw!**ڵkSJQT( 133͍w9|wn_ M'66|prp/M)^{~)Dn\;kvyY ЁqE^|},>ch)ä3\f.޺s%Mk""#|B.geصRhӲ9KΦQv$%is ֬ۤ$S'AF+Q*޷U687`4&MXZZ0|p?&cuիSPN ֝lؼ+9YG N>Y+יp%g3%"N7)T üN`zgO;GQȪHM< 5wqv\RȍRd H$ʖ»n-޽fڬ/~˕-Etw3j(ɳ~ʕ<IlپܘmW Rr,[4Qc&soZ[YsZ8B|2̜6sqhy+cђ8ti*ޭk_mٺ5?wxx5wn¸pmwĄri4pZ˗;s!c'N` YqڶjNRɴ9}CCCAzJ%?xYsesJt3zҵS;v=H `4iM2=p !Phj؛3.rCڵR%Kp{p~Nw+V[jnE RhaTw-5B,q^G 8~l"fj|r9q񘛙Nٿ{vvX,δlli&NMpqv"66<{ۉ|B.S6*H6o㿞"Hhݲ)%g|5<ܹ{RLz۷5WNj5fLeu9|$nĚacm'Q\wZtƭ;zmd^kB=1rB UDRBQ^ 0r2W91)[Dp~7w%bcT^2|x^D$ѧgZ6oL(n޾w/2!8$뷰g!R ~Ns&8;;r9|/]co=PYb &XXhᴺ~._<̙n]ڱxzvQ@.7~6ւb(S ѽK{lmmyc.**2oޤ! >e݆}-Rkذy;OhŸ|65Uo=prr gm_PfUƏI3hӲ=w$$4ҢY#6C=۾sGx6(!#;o"uprr`?ٴuNٵm-#9/ܽO*i]###=~U[wΞȼtx ]\h]}6#GhAJˡkWrmtJbW*<~c``@epؘLLMECFa"-5j.[Aj &f5 ]*Ǔ1Bɒ%Y&kצ|ΝGGGd֊Zq 5ͬ6Dԯg6SB9ƌ[/s9٭#rbjj02}^Zh;ƚ)FyO`ÑbZZZ+'&IPz|/]ֳ޵=`۰Z50_/,^s(r.؈9#ɘ;sffz}++K޾ 1 g'-X,7 ^eX~ /]cEw9ʗXZZ0&՝s6tj߆}g!ڷmIv3ӫ\)ju[avޏ# NC$q %܋ҦU3Lg'/\wu CJSxQ&LɸI)R*ϗ77FgMm<Û6#O=ΙBrr2 ڊs"~%5LbYԮY;/]$'N˰Q LiD8łT*'N]T-t&.>Z5so{5qEV˶RE/oБ0Zڶ$<OխޣXfx1}XRbi:s>ؘ?"Aܕl_vMZ?>PZe6mTN j>._׏4_^;q#Μi#oIEs dؑzt%Kƭ;qzҏc'ϐ°}12wط'*hݏOٝ~D!gȠsΞȖm5=BXXx*qG L0/Xt%݋3y 氵c֬XA &&_[l[5CCC]٘(̜:$͘ǻwL;Mkpe/XF >\|fM|}jl:-v̘2 ammEn1oթWYٲ}.? ߁ ~iy2BNUt&D,|d%dy?qF/3zvL˖d*0+_`^ԎIO8NY򘙙{زq#[[7o讛=c򍏋OB|#[`yykvj5ϟs~m߁ݺ ׮^j~5AF<~JQ0.]n=z7_^ z͚$&$ nݠ[ 5[!5jlŋ8w … Fv/qqqɐ+,,-n˖&6u;]. (i$GH:P*`II$&&Lrr.зT*EVhtqX-a)=ukV0e& agxh#E ʒ gsJ% )9 '8$ K sX[YFAABRg',9lm8z4!!!hQՄRN ,Yl g_D"0bh*V(GpH}0t丏ޫEFԭ]/av Kٽ nHI#o/\n NLL,y\sRB9hԠ.eD"S:4oĺ [;ռyxmxܸuБc122vjئ=o1j ϫ+oE)=~hߑc\ɋs>:Q'ψH["CK٧;U*%s/\K'|ObcP899syNfaֿOwݤN*Ϳ<))*ݳ@]&ko=ZaXʥ{8#)NPT\r F^JlzHH(ܣgItum̾%Rҥ3_ sS\ $b1U*W7JIIaq'55UNjh*>yKٿ&w~:c5Uf)l޶!Ju ,-|‚5R̳ l?'H7o%+$44BxfU*l:#(^uk G[=Ԓ%sm߽ sfLԍ2k ۿ|\n]>Jq'̓Gahh\.Аi#J%.N+'$.* z ?y/^&:0z6x@-Ϡ\ 5XxY ̂㤜.NzmPVINmst&,<3e:>۠_}#/(ΛWoP,ֵ.NtҞ*U*P(4.nАRJUbc˘ҨISJ%+.5wn,,,T*࠷,DN2~"1%J (X0>@Vc nE!PTܿw%1M} [Ӄ+/Slٯ%K"qəcc#dӭ688S'Oc$ik;L ލ TV SZ#%#RĆWL^b^ 255E.DLL VźH$ba)=v9l  %>^X5;Xyv.TU,㿞MUDFIHL[4W8kk+ff8;92Ye֝˗KDEG3y\z,Ä`A1HQ'2ݼuɟ/# 88?M}_R}Df*H$ZظxbRi)i$8u8JX,8;eZ.i„R$**d%@a۠o273#<1<"BOy:Po!)ID&}Z<.^Gښ=_vj\n̥+ضco0((` vJe h],Ydiaa0I!(8CCL3Mukr=J%j 7ovce!߼o&*q" :uh;sMk~7iѬPÝ#NIwƆvmZ`bb›z# ŸtG ﺵU KzkV¥+2}bhI;]@~E*2rv@sShj51q[Nxoy0ώyf*ficUz8;Lf?bbbh4(_Iɤp /t(gWұ]+ƌZ.pQc&e|@RRN$wQ)}=@H$"gNgʗ-Ͱ}2wW78!2%'k3 FjYn֤KǶܻM[w VjPrd.G[fν[^;AztLJ3X}vlgG9o]w׍_zW :!щA?$PaC._ЁΓudp񳰴@^t!M]:"nEqT4/_&&$Rg(͑orҥ[w>Ͼ=ٻOɤ+GIO|/^JYiˌ˜zhOOI"Ǒ#G8y$w͛7acc.Rur\4oof+@FtJU&,_Jpk*xso>6pZ(g}s\D"1c'Lz&LG7d`tm;vK1J,[nOF̙1!G'*IlSpA>"||xD$Wݠg%yVB5omDkhd2νTV_7DEGc.zjb IDAT>]oajh26KK |.\f:c ӛܾKU39t\2M;]{"_<_$=~B07gڤ1؉3TV5Pj=N>GZ5Ѷ4eY^;RVu wbLƘ Xxg_h>^bcm-N];pF1WD>z;νߪE-؉3LbreJ}1HߞSSjmQp[5_^Wݱ$  ffz .YR>*tt]nRiiᑑ(JKWg%TEZDEEce1Q(:߬IɴtZ6lڮԳ133Es~<}tT xDD$2 G cCCl_ϒ3x[᣿066B"1Ur9[jW[PN bvˍyȤis2b,ԯ[+yW$;h=K"Wyn߁P,\|y\4q 5k̶FFTVq.?Y͑зd2)|}33ɯ\NRR8$#WȩZ:z[k߁kgVΞELLOK2}~6?Yz.NDFDf-2$&&pEI@;LHXj `РA̝;___z͛IJJXp.XlY3b>.~ev͙4|.^&9YIܡ ;_I![azt@Isf[̙L`԰Z:uxμ?ظ8cTdzi1r0vn^ݚAn)˕[w֮y9utݣ ]syR-y *xr<¬i)^̍ WHNVbz1rΛNEHNŇW%xGח֖C䨝'&!ɤ^hW^]؈U+1sxN'quaq̝;֮^UWpvqsT,S-Z޼y'";,՚ݻ/H6oXOHHR L `9 ț/.hx.^@*Qpa.CbB{ÇmDOu{]UT&# RIttOI$/׮i۞75`AI@;s31KC$!ILLo߾,^3j(̈`oߞ+Wb``"Z}xjJ(N.9}'1|x tlߚ#Nꎝ>Ù(WeJ{ժd y@5K\.^}fXi!bbciXk!o”Y\~;3bH?=ḡilߵPAXxX.gҸX[oШ e_OQ<+ѵ+g,\.^]{ҥc[uyOύI)eEFާ Y-5i<,3θ;u;}p&qxSΝVC#kkBJdg_gi/^1xܿa42Swp/V%^sg_DLaߙ:^r):vnطkl`|m s+m 9[s>jA;GG{.zלpU-~sxoގgl?6lّ?Lqz,]JŎD̚6Cp {Ǹ~ ǶnCn:vt |Lqv麸8!I;k+}JB O+Fo]vMZ?s\BLl,]:+Qg'H6la#x,NqL~Èa߰aÄ3uq_?qGнK{LMMXl5O>vjd2\ҽ_ĴY)WcGk z0Vemљy&cnnaռo|ȧ͍[w5o9]pJy`uEѰvlmiXu)Tիr/[7M[wDN(^̍[w1zdiݮ@4_lݱZ5?]=Dh^eSa3ig'G6[{ܡ-c'LYϛ3g0wtTT*vs׍X\9bbٺy3[7mjXE弼رu+ȒÞ]6oXZ#|Q(Vܝ-7`<"#"R5 s&$$sfs -qqv7X9+]vPңdVș'SL'gY` HDrr2899 :w` 4%K0vXʔ)}y*1yhp 0}f]ĪX|^`m*Usx?dhͰhwe:y1-L)nݾG Y|b^3d*0Tm۴#[s"wfVX]rc@ThVh4SݻZ&111j ibL&@ّv*31}֟Ɵtܾs-;fE@@Eӆ/]D oq1<>'4Y4ϫ;7PPS!H$ >zᅪJKѮ}[/;ZI@; )Ao_39B ޾AE"IIIhAg&0կ_ݻSB!Lj 0_Y@@eνl۹?_ (mQ^ 0e6`$1FPJ'.|#_ H?RꁭDԬLV-(R .  I@L\ m_3s166FRaggGPP]|Ջ+"uPI@@@@@[x?Ew04fraV/W&Gu(@x׭FMqss[ h1sB( h@N@:*ur )o0%Ko2[1JGAV#عs.wL?4ڷi& ! ~B>"0Y(RR~*4PM 9?Wrd0qvrFL&C*b``F!88HRj{.P~X t+xcgg'?`$aԍ4W9=I^hJ wD"pᳬubXI@@@@@@@@@GA*йCk4O9D&"GFNXJyo*:Jo DlWPUcz  x*gYLLMyǻH`S(ʏ 2 D|L`Ҽ1޻ƥw&B, Saf{Ԯ] @~ I'EqE9LĥdHQ!r,XҥK;ԩP ? $s^`Ҩukt.qtq)T*X^?'9=wF" #L? YM.'MhRY0)uo;4%HB 89[׎TRB!O@E&G0u"|B%QZa~8դX B] zh4j@ r*P*Q 9o_ ' ŁNQjrP ?lw,21G7_;t|Ld8wшĈАbnM[iwF%kgQX_2*yˬL}N}-4اk  )0D"r qh*044SSSD""JELL qqqHl"CZ_ֵܹ#*WB& " mIefAt$)fZi-?Elq/:@]+0.#ɻZF$HMߝ]4Z+&M&2U`uhH5)M(i$^I)ShhФ$`=f!Jy󆠠 "1Nv%11RITTM 00Xz ܬAjv2 {a$ޏ I@;V8;J9r$ mK1Ac`>r"nkmlaFl@@"T*T ,q `\'tCd;g,> bHw!ׁ$prXaD0D3ҠW8adqEKּN:KHoAgdrǒ׍V}P)̐D`} F/UћgT[Bv2^i+lw 6TBj|„)z)ФҜysEȌfcO)&N Nss=!YaAؾ͈ay|;`z,Sz^d;g '%bja4sD$Ƕż5dZѭXu4\f"|TZWx2PqX;}+Zc۴AJs 4d/~( j S98c-J쒛Ao_gߪIz/FPPjZ}6/_'''={X,bŊpq nnnYթS 8vB0rº}!X5"+A$>@$1FPG ۾ Hj+DN)S4ZT&N cmļ F;L1@enEUL1CݽT&fH߾@IXӮzǖJ]218zm_$#tqBŽHʙp_qߕvwB-5Bc`Hxjc2cLnJlq/"kř}B&h2Wo;UdzHPߠ(yδ}-& 'G#xbb1o߾ɓѠAj֬H$ի8p UNR( sS">z6L2ŋҪ]7<}u/CCڴjF)ȟаp>DHHWG^,s&ʾG2/W+7I:ro3.N޷UUH$ݻ(M6erEN'}EЈ[Y䦨eF6ZQ,!K癘HA|,\A\4Јs+z]5T."dXCFl%0ME3\\\ BkMMM)[,8;;3i$D"ZQ IDATyD ,(šRP*/KiϒYoj::3k^|ͦ-;y1v9U*;6fٜ:sL*Cٱ뽨דvBBBXt%۶dΌoögQ=CK&U2#tL ҟ=e~+lm{oٰyGikK5n x)cʄ߉O`= 9R^5tMk{_jWӉm/_fi"zBhX8˗eڤ1t5@'$)T"%)1Ҟ%rМz$'+(t"#"ػ{Ǎe),J%f9z~U{!ΞSS3Zn㖭[i:yR:),, Ѷ}LLL8| gϜƻAC)FÞ];Q6yeߞ=\z }&OE=PTlڸ;;;rɓi>T*T'28~ kkkh+W8r0۶EsUXF"Cml R)j"NG I{`r"Oj|T SLAjDH1lw.ED捉A| DR$%)C ϋsՂA$"פZKef$K$<&[SqѨRxi`m)L3UJ˩0ͲۜR ߟbbbh4*TRÇJhXz5jooo E$abb3C"T|26dvl^B!Y47L1ojISw -hm`ư!6  -^Z=G@hr%y=;y_€=n4OBBs/mTǭ[ь7R)fwV v]n߹Kh#Q7nx,,,,ߤ-jdl6mΌY?3w4_渹gد)A̽0'""t׮P ܌?v日+h4xq%<琈ńG\CTҦU3X"]g{2_bNg'GƱ~Hڸk1lmm[INַhZH"2ws,/rTVK9vn3dPc4oӕ1/.^gu!Jzqݻt 7/sI.Z5opSؾe-oܢ[:;zuZ*AXVܜaӽh(f]H6-t"Ӌܻs.*]mTj'(N:y޻"6iAD?|% E999\AЦmX[[ѧgWlQ+s.Qf5uMG"HMMgmZy{Kv T(Xn=f@ߞ-B@$q#֬HNn.ݻv\숋KNt +VgϰXLzzYYYL9iݢ3M4aJL0[wY僖b;_#O`tkblL*ERR2Jt܎ʾHMK-R$j$~/L+~5Rƚ>V8j#GOs^fMe+@0kCFڵcƎ{D ҟڐʥ+WUٷFIObXn;vA&r_)..:r35mfLiicƿ/,<ſǪ_RӃ?/f`,^!V=;A,? cc#@/gQf5|2tp? ҡ]+,-ٸ~YB+xlƠ¥L8"CVsm֬LFFlYoB.P\`{Q&qQujTq@>o&V100`thے&zo]mX߂m?0r[: Ϳڕy(JW$qfV#V`q( Q0 ?  *SsD9Z-Vct*Fw"P'\ULb." dV"bE:yVv az%D+ w+b80h8#H#*D"%Iޘ'&Mkdggcjj? 6N:x{{RHMM% gggiР4iX%4nܘ&MPJ,--Q(⋡,SXnt\o؆ 3-Wև߷řf"8ortz5AXYZ0gݵ3g'$۫g.`gke|YeKk}BѰt̝CCC͞xnۺ ЛmZV:jz)kW.fp+@N8s|-ݭx!I,3|@N9z/ԍծUu0HbnT{u%=~BhF}=Ior5wso f5v=%'h͜=<ǐ}:s"}zve߁#|1nݎeƅ)'OuSτ1E"exSߚQůG L,_57դQ}vGC4_烌M4 qv46mc¤iۀ>=~&'Ne%i cc~MՈktl굍aCq+'Ne`un=}? ujkM7n R:'V^ϋ1a4ԗȨ=~vuX\lYx90jTSɷ</s6-1np:Rd/!Ж!nG, Q6qL}zvՅĘQ#>h8ͦ-0Q$$$Q\uІCP0d`JǷb9M@{t*V`ڔ Fvٕ_WcHOϤG׎- G]F{OaP|VNW֊GOfMLT<&NcаZf5Vݠ+ J%낶 Q+ܬI +[ȬY3 Z|=j.*֮^}{u];Dڵ\8-L8@=/R(Z踇\/Gӳ$'k}{#^lۊZnt4Bѫ7G {r=|/ӲUk8s'WC,驷Toؐ,߻ǥ )[)ؐ?]VDbnUTݻW\n'H$--޺ի2ZRYY+,QYgIk9dfIf$mTPܾO3Ͱݹ u۝lO#q=G6yv$>*cS9>qlYd{y m~*1媒Z `ܘ'}b7,&|5^D%ͫ7%D:I(Uav䣰h$7G!(Jj5Rkkk,,,$'' %TX,F"`hhT*E$Vtz҂ Fop_1~)fY<.^F*w-o&!iܰv#m[7~@"엹ع1gW(_·vCc zvD>hJaeiK1gNXZXd*8T*e/Pþe ,9q/5m&66x*zv~ԡ ZÍk6лGg~q MZu'\]!8~2D;s6͉ýZkW#pvt,\[zߗh4̞mWڴlH$iw!JEyy|X3p+Nr?xȪlP_4 oߥg!V+QM[yoq08FГhoD^ϥ7oEVKH VKNNz5N>3RoR,X<4&(|033%=>fJy0FFqh``\nHZY0uH\@LL)V̉[XJp;+\#ǣR8zFի5ϕUҥKW\GR \r晀?ER!IQUɹIMS_*jDTivjZggr)uX[[QNjO[ΨDʎJ¼Y 5>ND9N\|"5Wщ#DDFwSԭ>h Ro&PdVФQ=lpB9w/\fƬiP-Yb 2];bmeg:u\,--i0~*W)['TԨJ͙h;s]^)(_9<3f31ˡ8Sn-XCBbʗiYVKWtwftʇ.GP|YrxhHjڎj*~$$&q52[w100q@n`rmf~Ygmxqz8+2<-7s:w E FCF<&>>[",\oٌ< 11Ha{ "UV|X(ɘo0Ąгg9q[B"F]Q,y5k255_e}qޞy[299;ӧ_/]BN1{n@4hٺ *+Wၵw"ɐH$>}7-8HL|LJkjn\W#G"H([qtUɯ2gRZnJŸ/\$o9666Ç)?|6P 3/ dRϷb_HF$B#3$R-;he Q"GS"GOrcAWY#Ϸ(~^޷K_`TJ wv#J"ωQ?Alʞ K&E p5>5 MjRINNyyyhPwFdmyyyH$]X 0iR/%5Uט9{Q 呚Nnn޼};쉺~jU066Mh4:?Lŋ QՔ+M/V^ϷߌE3m&4΄q#0` cԴ4=~3C/Xa+ǫ4If; ؇:$$&Qbb2"UT 8W&缲L~[wؾs 7X1'rrrt227S͋ ,6ncވbnx4 ~cV 0_O|3L""'NX:kENn.ï/] ss2 W˫^ىs.ciaa92{։)Cz4Wݤg`$#Iu"`F9}& ~XgAdgk͢| IDATݘ`K1gn޺'2 Л={pLYӵ-J7\ҥJ|EiKǶ`?Vz>VϏ/W6?ff4hr`otJʓn}qZo/ޣX1',͉}O+||YŔ-Mrr ~y>Ѷ5-cef>cÿfCj5FFr dJMLMLĺ:C嗯D`iaAf^h:#'з'}zu҂#O0{*_ȁCG)S}{bnnFș0zuL3{JiQC'''ĨQ#(^ ښvETrE fА+͆$$$tq*U~fTf"3oٿoCzzdae+?:@*N,U+;;;{{OB8Bs`B199гgP&VxJ'//y?17@T꽇=!!^O;cٳMw\\HNJFVʚK %&&bieH$"77ZuPA5-(<~D;ރJBV3o59dge!72Һ>II[]N)!/=3=` LH~uu*=W8P(d2R)JR'$^<_X_`$Jx)1Ӧ|~h߮%=8, '7Kܡ-N؉ض]:e3MSg,Ά])<~7)ΥKq:$">D X޷]ִq ˬ{iԭq#P̜F<|U~IIXZMv _ '@ nٕϰk>ڴjFe lx%+UDRPINU7{ JP FɆM8abV !}fϘLbNr7mHvزuիnݎ&--]CRIZK\ss3&+ݺ9^چkIOw7B0;;-v0_Ͽ m I^͟bTgw~jTG"uTտ2‚tD\Us߻*~hѬYY\xZ!Iezә//]_G\+ԡ111&11 F_R/GҮM nܼgUU9u(Y```+'݊2YR-8v6ѴqN100` ܏F Y4 G~Ä#ֹ=Occ滩wȯҦu3._eJV,jEKVлgtlK\\<(4 :G`Ѵ8vXL!JkoBqvv=Хc[ w-uZQڕh&%%V-0q(:uP3j4VDmدq:/rMYYYL-N%yvo0ݿGWÇEXs>MjdӾ%~cOӬqCΆj&?l~}oeuӳ+3NDVc?~sJ6potcc2 ~Y4éY -7f}$ Ǐ֭.*_]۷f¤i/ C/?YUq3 ܏Sg1vpFH]W؉Ӕ.Ɍߒ˜ =Kۻ+ 5-WRض}=ub)(*,4UE_LFkB6o}ҕ;q~1u\^FݸxK:+041oTƥ+W*;w?&D^LVr>=zs>ֺk|[Iߎ BٲԮU㵢%Nn%wοz:q:$Thؾ5=u"#3sYW֨qدMק]$'v&RPn[ߊ\M$ѢUK>Ğʒ;͛x!&&Tc:ÃFM벥L~; |lٸD"hס]9imR LF-0tIV+R-[zSVe`֯YZۧ \ Bll,*_ۗk9f;ɉ*)h=j'ggZj7kޜ={?'Lhס#/oXf5yyy*]5k:U̙iٺ5ʕ,YZMҥ[Mrr2oBvV\\iնMcݳ,7ww3"m3ss7u*\Q4m֜];wxiݶeHf |"h*~jШAM^( >ߚA=3:Q/%ӻkTPAdPĦ|Mbm9ʕ)E^ݩQ2LAdФ!zn[ؔ?]N$M@@@@@@@@?AۋU`` tMeMvJ B5M5xX -&D TQ%K! >G(QG"~tڙ~HqI@@@DP=QBĂQ Adi$SlYDoQAdHiѬ>۷%OAdhբ!:uK OAdHک5۴S OAdgbFcSs*ɍqj<%HhfIyYk"u3J29  dG& $!KD65Y*!&>%b&{'jGY9$7Vh^8Jc=gC9cl72~qIxʵl!qd.XpCnr撚'܇B |0vjMNpuu"C@@G>*%b,C% w:@dRk44.fu* Gi-?; ͜70CI@@@@@o"/kM/omCfSȋ@jojH H\]jxՙv(DAdU_{EȤFҘLvTPFW7 Poo$FOW @B=D@AVclj΅]櫰P.=rv{Guie_J$>!\r:jxl>*:8ѧ "ӿ[Ƿ$cHG1o1F&{C1"3I/Ai;܆rJ{u*hh^s߀ըWUk6I{CggGٰ̜w]=mJ~1%%%Wҳ['C"};僄nH9)V +aN"Ō%j=OwI,7n{szL6-4e&7nݦtL- E6Fss@wN.lݰk~+ְ|vC~]N@dzրB^ϾE|B"jVcԉ +vex#Υ/. wIJnn~}9tn_彷{|5|gC32|+cތ8%W#~]NN{h;Ƥ-^Z +J8"[<_`EETj 94p~."ܜ JEH"jРA'i)[{ý lcn[D,^0 #K~]E.m4iىjUn(Nr'.:w@0y\'{ajbBFf&ʕy`ųymiԠ>^iѶ+9tܞ FρG)DNKk>yzZiS/J2S+|ywswa6oHRR:w9rii|V^h!(Fnݼɦ <~Lʖ+RF 9~PfϿ{11_ӷBO?Q LY Z!]b`iK >t~:)!!S'ӽWoLMM ޽cGмeBi{ӠQc<<=ٹc¨˗u& FR!h={x]$'0ddggi)m[@ c:!?'2T*rm}0 J=`zF31{is"vF@HKMF,#Ҩ@ZI\=-ҀHA#q>rr)U$NyՖ.u\m\1Gm~jd_€=n4OBBs/mTǭ[ь7R)fwV v]n߹Kh#Q7nx,,,,ߤ-jdl6mΌY?3w4_渹gد)A̽0'""t׮P ܌?v日+HIMe鯫u;RҘ1k[6E"0cܸyR4WTZq[}zvɑq߰{R?6.(Z [[Dǰ~VS;LMԧŋhRR?6G,[ ԇD"#ؾ5tG ߋٳ~]HmF.c9v~Vj<oYl( IDAT{β^ݵ JzЫ{glmx4x33lpݘ{DDF1{Bڵi^p 'wWW钴hM7 JzSNdjޤ!GM~i% E999\AЦmX[[ѧgWlQ+s.Qf5uMG"HMMgmZy{Kv T(Xn=f@ߞ-B@$q#֬HNn.ݻv\숋KNt +VgϰXLzzYYYL9iݢ3M4aJL0[wY僖b;_#O`tkblL*ERR2Jt܎ʾHMK +2 W#{Ad\𫑺6Q9z2kd/[0kC"d*v0}'zupt'-=t9tkIgm/?_:m17ofdddq=mtdJ AѸ>kkk߻Gnn.R;X[[M3OFT ,܂#v*FǵHm~plm ~saaTZϿ HJb?z:uqy PQעȠ~Æ<| -[х3Og!H(U4c}ѣW_ςDR˂XX3pw~>Dys012~uŋŢ"nj5fV] YD"Zn屢 9u\(AvJu+_Ajj*{w҂XY[S7%"x‚MZ8ϝ|X(^>֯;)J%u,$\`\j\Y UU߿5Ο;KIKM۞DtiL2\<p[3&C/oe5.jpM*Vҥ *qw΅q5 "3#~rr9$'%GNN*"ǩ'HMMozTj@ǎXl":UO FwҔK?;{o͞"I$cWUJiU*Uj=cD Cd=?\n#Jڞ{ys~͛۸ys.mয়&beU6m3{v;}o>=eOK 5 RUx &S} K11P_}L1Зbh XC= 02@cƆj)MY7i 1rjlѼxVuT.Lnlܼ ,[L!P-bK}m5Ei :)H HH)$>9<"bK)@,QVkE)R]1Y9 rHuŸVRVD)G\r> )i`zkæ=ۻ;e˰rZPi*~m[~_kVLѸuuUf/Wȑ077-՘\Dzz:OFPeU7 )xZNMjVV g.EF İ!S]{gDD>2;wS L6lEPPNRw+K 60fdddѧWsScCƎMoDžwT \NӧĐSJ/ǣ5Q* z!# KyO?v >'D{i^H$nRIнngGV|5v,v\X|#+(4Pw042[- LW ;;G ?x);mc=n-{vRo ˅sgr['ם;s׭ee5Gb\xu!jS$%%[m6o@TFcGٳk;;// ׻ W.]}ivV'W=l;Y32z8::qᜪm;zccFu{ O<Ƶ!<:׮}|G?{h׾'WTk`a8:i~R4i֌ܜ"#"}ŮOKKlnN vş>F%\]]VQ6͝4=|HDx8N+#5,c^qGL::L<.]ǩS\Cvcn΅ 酏O/z]]Ǝ_%eab-#Q%>9|SK*FVF1щ<$ RS }4c $Rxd"#'O"HLHBO1g*qxp ?%77qNalmbS6vZ[wiݡ'q񘙚,koRk*:( .Ε051ֆnҠ~Lٱkg_b}.C=Ӫ58BAV5ˑCQU֝yy 4mDpBϧj7p""QPP{A􊕵+J/^Z+c;U!Q-` a5--R]3>Wޖu~6Q733 } [۲$%0bx[~b1ս=r9WiN$ԩ]#N_@||w{]Tr=ʗ]en hPӓr70*~Z* )faP(HMSMZmlvܗ=w9ff:sr?f>`CJ蚢4ޅ&哑-G"Cmʚkc(_@HuDka(/SȤb,M07FhʕR- b j=~w0wdͺT{׎/ǭՀb0oƌA*MOV1UQ;66֘r' Ϩ߬YÇĤdpU[BVV6_|>ZĹYh2v66\sssIKϠm\ժr]G ٳ >ޞjavnjlȤ _kfzNr᛭RKȘ2e8Г(k2>k  R$$$OOOԥ Z!)160{t6T-^$$ī433QO%^'#!><5oANyɂ%ھ}Zo{-CڪNF5 y… g#~0aǥR]_PF 23ٴwd2]x{ըY5kraΟ;K6m֣'NŋTrvLɍdɤ4n\Ch.|[-UeZҮ]m)_ xKeXki955gZO-Ç`u4oMֵp^2#'gfxV҅S4_f\<~Χ:gcc@Ռw*%ZA*2V_.l+8c} LMMp*ΜyK\ jUKsMF&zRF]hټ 2233YxeΞ|< ܜ2e,#CC̹| ~\ڒx{RAKLgNTpcʌ Lb5s50оtn)#7_Ә< ) ˠP 1ȹɀL]H|?%7OS>ΞZ}Kw[&.ΕP(6&L8-jl򤦥5Wƭ1ƚh޺ {#;+={^1U ؘ[X}J߷&Ɯ~!Ӄ7Ѡ^2~KlldȝAdgнkG f l^3Jiy+m ~STbokЮm ,-4&VUx7T2>qX\k 55'#H`O˙4a: sɳǐ}ҹQ{$}'];q%Ξޝӳ+Kף?gIL9_mpM+籄U,_Ѱhբ)Æ `쩈b>]]ttuK|ϯ|"J!r":re232ᏽ{ )V.2eT]]ßRPP@zza}Ą׮@^^Wboo_lS|y23GTrZ/"ٶy3q }Pd\|̌ Ӹ{ʫ6Vmƒ՘p vY} wv ֟Sŋ=W&j*ϟ'$h766ss? c6ofddP(HIIPn޼Ϟ='1 8t<o(<^llrBD_߀_),s]&|%u^ yϜyf#1qɄ?ct'u,9a0OF0yڜ7.EVu/ứ8[߽ v"4iT/5%&%3j̷z$3~KxDLjt==)׬g"c?""ո53wSg=oq Gkߓ:UkJy 0nU%Z{7S7o{j˥Sg٭e8|$ڵ*۱hiir:RSh߶%'{6ZVzvĐ}OHR3DNNM2'm6ni{̗ S¬i;r%Zh8wJ^֙scz}mB^̝9B}x<KVAA?޽/O~+&uN uԠ]?xTu9D_{~]֦,ݺt`ҔY̓3c4\Qd¸\xC2u<=Oo;pX#@vi9{+92gwrcqb٢9jZr9;߯; NP'Тyc:Vض{:?99ܾ{/13.޽ ǭwK^ۢ35~_Wn߽se aBAx&ZRfGZ:Ҍ fp'#bk\Z03fh}GG]X[h $3tp?=ɨe1Bl\~?wU1OiO_wž2m< GɱݻtͅvkL~Lzus/sN9_w.]P0 2/#=~ 6oEBb"]w!<]1l0c'LVcԈϊm$$bM>t 2sEϝTKJJ2R](gzaӓ\$!/K][U}`goTO\x'XYYQ|y"#"e+Ul3NqQR=֣j_rp>^ٳЪuΝ;˹gߠj/V>۶{榈qI8 :t,'o^/[JB|<4n^*VyV*&O^M-ZrQ ;YXXpA2Гpu{Ս ~0:ۗx>m\jycZiÑLP]EDGGY^>lXXX,EK|m- l(O7/׮[/eR'[7mDPRƆvۿ۴C,]CC:wD"ŕd6_GAA*W.1;v\|kkkuPG"#YG *W`ㇳ )))ڱܜliߩݭg/:HNv6*3OѨISu\7iÇX7 ήUYj݆Cp*TCNx+23V漻?D"a04|i ڲ (sym:={׷kE%KС _c}C~,křwn>Uyb 4"AZZؔN ԬY!)+TT*ԡn-nܥ0?j^^ ,U,["Ѷ_.ǡ1rn-(ZthֈZ5|8|$Ϙ?g.kܖ( i҅f4u Rw3 O#v- 1%ir-p毣paކϣm} 8;Ulr|\uqQd&tx$ 7&'_ALb!u@ƽ'[bI4ϲEs =iddiu[%Mu^k` 8~?.k㱴(q>U"#"|z@-r9G\GVNJ^e244ѱcc?KV[[+TDÆչp&v$**Vú)"B3ޭ[s9o5M˖sZNO&OTr9RPJř ndS]undT^R/DDQ###^F~jqqz)oo\$)%*nhksN5/m굛/,5ԔԴtn߽ E k]rcz Sr H_aO&d򟦣S~NיtuDbKwJ+ȧEm}\)+qw"WR)!Ii ,L2]::"6APbf,A$PL  7C I|21d7WgztD^Upf~ԬYC#dȧ[sNND`{\9kjV2sTRd#}LrƎ]w _W^p~3c¢8q q1y0FC||c.$;}kw:+|N B ɬt H1J=v'< cb_I)(Y[eg'|^l%Z7)a.OHdcFK!ӭs{4aͼ(+}?D9yT=sOgLnR~܍l$r?O&` Fogp/ kmU-(vn63%LLV!u7CaZRIvP`Ut777A\L2u2lt;-WӰdЦUcv팻KjI"[LukYvgwL2CQ(}ftggg0>%e+ Lzv@Nqtt C@@@#"X2 +ս: ⒀߅ 2 սݺužP#$ `@t kkk0"P1 A/D"y lH?ZhP  $ bjlȠAlccc@>aI@@@@@@@@@@̘a Yf " @>m4/ 6@__I"00K*Z[$c tde$ !@-S))gHnpPH#2`?5D& " Ddf͑ %!/BYJJ]otӕ5<͂X V@@@@@cgq[t* 'YPh m2?GWWW(0$/Dt%cbڪDž34T*o;wBuG!u7üj|M#+ ܐd sжCj~-k$f*#i#D~}S:::B ft$XR֎QOKeT$) b;]P(DD M҂fOZUzQؓyii׳ սQ# I<7AѠ^V,Scq`bׄ=~Bҩcv+VzZeKaJwH;Y⹗A__F/!>`ִItlߚ=p82|Ըםs.ף ٓXl9or\]SS8v ?,XB`疵e->t(J0=ilVGDӪ5xE#mC&ӣI6'i؊,C Gz7a )iߍ Y<^c> כdܺ}/Gųƹ !ÿG';u=ݏSCA|41~2u!ES2i˥+; ؏5k ̆dUN\tA}?|7=9΅@R5UU+(C?#';fS_W3o>vvoװTٙq&|<αG0hprN ** SSS<=ѥ{wEIܵ>>10h{Qlߺ5jOTÜ|d8ӧiԤYY:xgQX[Mv"o,K%0*4}caRuz L\.'77mm?YvF*P((((x4|֢ko&&&5 N'2PXѼi#vnsrSSXzuixehټ |o7}Trˇ.)oDŌ;CG~HL{cظegۧ]:c> r%'fOܫSNS5ٱcZcWY5b[d3~mxTĂSPP#ǒD;s2?/D;w7tuͧO5:9Sߗ|q9o>^|{|9jWntT!8y+WCʸP^ʖ-yOm(0__ m^ۧ kT/obRϝyt4#F~{tVwrʞfD2skb[6md,o߱#N337e֥f\|SщdسS0}bu% R?'0]<---j׭+t$oQ&;os/t1ҭ8r0zD&2 MTדXr9+WF,GLL jRI,#ҢGGGGmeӛtuuTM\C-kחq:uhCdT4s-^p_ -[4!11EKWЩCjѣ'0DYt> |J\%?KallL[?g=̙EfЬIC|E%NJGD2cBCiW}022dރj iie93'-=9\ى][׽;-K^ؔ&6.&cogK9{[,,y$[v`jU1Et*aܪ\M >hҨ>^(vܯG}֟$C֥m:"Y4 ˭;:rH%lڲ=_PK9}/^Z :Tl7TmP%}{,@*9UoXZǚuxFF:4</۪E?7oSĵkUǹm;QXX3qr@"S8x"Dj죈Lsf| 9q?$M[wbffJ^XYZP(ca߼MUUDŹ=wFKVv67lYsͅlD"QѬ߰|zꊇ+eX'Y1>}}bLrrr9wkUCV4iʬ7@[[)6lQqXTscaz/1.ΕHNNa݄> C[[^=:]*i?qWK Lu?Lޞ V܌}{P g.͞ʊ#2c's7HYsI oަM8֝@y4W #`:'罓$u4ARwOmO5WBޜ9GN2mxjܾwS琒ʽp ԯWg?qD"F DIIM{V_ɱ"q۰+qqqlٶy#~N]Тe+ IDATzaYd6Ԭ]n=zٳx_>ؿ.ݺ0ix 擗KXp!vFسwϏOI-ڽza`?Ǡ냐Ν=CnNӺMd2n޸AB|qqq`kgOV047=9!0Jpj8/ѢI, $7nSz'OgߞRbEΞ>͂%K}TL__u9lՊǎ%|Wgy 'cddĤ )4D%ӳ(=BjJ*бUY|||bm[bbl̄靈rZu@ KHLML016@_&%\vhߚdN ѺU3{/*nΆgdfw3mU&œ(:wh˩3gX6o܈/G0nGFyFܲ͛yc\MCg݇ǎP(HOOѣ :ISµW).*bǶmtwgahhā_Xk2H$&N*Cn pkQ8cblDm[9A-162bok9pLƜ3iӺyG_`dhHt=4~wػMC\|F,zvvD"^¥Dݏ&&VUAܤX<T*'MʊxWzA*fGbb"A7)))Ғzw.D":uc.ii8:hDJהi♹֙;5qqqdddT)LLLjqyF]]zx6#MZmJ"88#GU;tvdD%K#ҢU+HNNf/+3FP(X4ommm=T[\p&MՖ!{zԹ3VeӦ!/7B:Czj!4Æo`@Rb"1& X=::rJKu ƍ15UzSR\ۑXPN=Ѽ9?/_NII eMƃ'QTX_%ӢS^!V2+ӓ033CRQ^^NBBFFFbSƍpnnn( U%%%( b1III?[ӭps2rr(-- XZceiAxm{7CWWGR4q굛(JҰ km⓹ӳz" |Mogޜ7GPmM@撐ō<5Ak!Y~HmE@r|֥:::?y/5:X!JHK9䥊LpǍBWWWU_#CKJ(~,VLkD"7fxx+W`Ѐ>:s/Ϧ>z{;[qs33F > I(JMmmmӣ1/]}浇Oٞ]{T l߹S{.^yqo]s̃= y\۴sKJiNT*hnZ[Gaaf>x(2YYZV!%9Ocqj恀!l$5T 4st+]==󼘙?k 4kk~R!mJrr ?B]]==D"&&ck'YkW+4k?9=;vdԯhMfCP^"\GG#TSJ%>=z>~v̩Sbtz3vfD߯C#XYY?ceض5~O翔xLR.+ lXVme%RHHHYYQ\\LQa!:տHvWXX,F@"cǶmHRRS(,,#6qJEll,'T/Vչr]R1qٹ>[յ../M|vPk²B{EJrk~_UA,+-.JKKH$O}WϏOB[ " AR=DFF JddggM~:>Jnn.yyyڒʽ{pqqܜ 9%.jT:Lʡ k+Krmj`eiS=_~CF 4~]H~AathKKs_dBr !c'M R)Fmfnkc͕,~bZ±199>5:@qPjW{:~^ޤ]-ڊĤd\?9ƏX,FT2i(ib1WB&ѡ]+FR VmjPϡN+ͽQT,RmR^^NxD$/o&<s004Ը!?i=D" 41%jZbh.cr%%U`xP*hk.Hh®jWtbȐR133#.6湟4_>o/LvT*2lܸ~#1y[wue&M' OOͿ[jH$߀,9\!^ӋWNݺDݻGXhͼ_zf_YJEzz:Ϟe)rMLmQ&kÆ ÆUz72gs&dg^]Ǝw}{wӽI 9gBA^~ZʋlV_/[d2M5dl~㔓@tL,_/Y5r>Կ7[LwFGV_8P( ؝Qd{un&ܱ2ZZ2Z4XH߁CGiӺDcuhccnߘMoxy2i(C&{tC,̃ݻPTT̵A --f|{ܙ/o1~CoUtDR̳)榚M0IbdhHvu!JJq¡=ee/M{YgUfo}0 s3S &~` yy"JYBMO g-sqJ%۶}aS;{{.]X222hejk7SVVy]5ݻ5͛S^^ΆukxWй~}"IIIATWgZ'7Us3[e 0xyccv7$&$pA{Ц];p;2s6oHdDb]]ʫ򥕕]v㻯AYYiiiۯѥkP&cԷؽcIyќ˗.]LDBqqI%Vcŕ~K|W´;Dܺ#ޞ[aW>yehnGFjJ¹sI툏Sesss()Q #BIMٓJhH0qQrssqtr$2"Ą ejjJDxă` AEJJ 9|3腵ALs}g^`?.7LXXԢ82L EA@/(.VfQST*8>|psO[J_S{/cu?-6tU|J6Np>Y%<ٯ 1eo~Nr:BzF&3ޛh\cܾsOA |}{#g՚;Hdrrt 11Rd55Ɗ_T 4Vbc9RmʳJ(J{M2x`lmzꇕO/gO~T*eudgлg7#&Vy*u:Ə$5-C,ޗ" 6֖]lڲ:fvz,ֳywT#CCcb`BY[uhĤU:gLΞȴ)Yh??CٻPʙwvdɢO(++굛\ ctul' ˯֢P(ؾk曥S^Б\E7ڥ#tm2i,EE LyƏ΢rcsFՋ_;;[E~_jnR|&r jQL:O+YɇL7[qssmOoŒZSn9N>=co;L&#;;wpiLjbI{xVbf}0 ٷw/rmm|`А!Oŋ߻3``RRR; Բ{0jX>̥ pr 8jբ{Ϟٵbjӽ3_뢦:`J16E LֽO$Ue3//'HEV?{az7`q IDAT\8w ŗK\cD"z'8?&ƚxZeА!XYYO?óWz͇}R^lT)* wcggsgcd2zꍙ>={ra\Rw~]vvhɴرmzl݆KPcccMБPZVZm̧G80me5& !6@[.\@mڵC,a2--TJ%]3gJAAy89;ϰ#,*hסnV~}yy!Hٻ'?/+kkz:r`>V#[4bָi)/t{eOԢO~9GTSU\>>m37yyLk|7ȑs 7wE@E㫢˭L ls@³4RbD}n Z4B*pBD_d2RD?s$1ƏgXZZ 5BB>. PˉiݫO3}OINN'?/+={z\ [G+W~֖'9qb5[~C^4iΞ]ϛo6رUmی#G~}l$ԭk; qcuQ++SV3gֲjܜ04?tu_XcVRik˸rss <3Jy/)5@{Ip) 0f ;0oPOƦ-m+Y`iMIKKIٳk &T*Lgpuuxj ~'[lݧ*~beeeԩc7iV_OO8Ȕ)177gw=;f:tO={ L:w#⒉5eʕ0:ua~6J;wbiXmlOÆb6t$>>, < wEO}q79@qX*~.Zv~ ۯZah\~OiiɘDc Ԕgk*JqssPKKSmbbk[rT 5uk] #''=۱n^JJJK]<Ķ P*U_ѡw%7ڵ-qrc˖C(*ΜBqq) |9W쬈K&..''{6uҥ`_*)f|B438CBY T(PPVƁf&1>!`QOڪP26EPt#GlyV1E7Aڪ7{2rHjժ%4#TpJkW4mԅ+"TT)镭,-MM"66󧢣Mǎ9rHE-.|§O;5ۦS0iR֭K7n~R 7/VVVX,"00qLprmʞ=#B8]'X)  egy0a(u튑^!2U|CЫW;MouZ'sJ}v$,n߽ΝX MOxT*r: ZږkxF=`Xm4>i>GGG "Qπk#f T/X5 ja:U=fPLqx&eE,t7^s+$Ҭ c/<AdjbPc! TM*ceiAii&RD,P(HMM%%%H%H$J%"HJBKKN_F/X՗ po҈&qn RCN}gG36Bhצ%5.\+[Ǟ6W:(7jFƆb?r?:極+#{M͑hSUBqP9GbL{at6jwXصzNZ1+)dWmzぎy[s h7zOd?=g4})e c_s\8 'cffZOߨcM &$smcZ4aTU;r٢9u0{Μ^4zf @ͬLXӴIc 4D"A"P^^NAzL& tuuJ( F]RԒ{eU[nVnIJJyƊo~Nll)))PRzGklvˢ;}bjj6KKKZiCQQ1W/5rZjISF())ڕlZڵpmРB^n(/AJe_ʹwcaiILt46'.6q&V:K++A`Tf_rsnނ-ZסS' ^{\VV7  2?vSSS @<İ#I@LG=`/]:<ۜ /FP\\H$BTRXXHZZdff5bl111sss4^8pjk&mmm, JJKپe-zz:s~}ze?άS8n[NgݏЯOь;w`|Rґd8t?jʼx+~2ԭm[d۶%~e_йS{ZwAݺ|,K%4,\w 8~$}]CvNNf˒e3_/{n~}z`iiNJJE++[[kƎ)lڼx:uh]m[l133^T4l#++1E 1ҢIT̝NrJLJcIM ,,AУ0آ.o8<-҈L=uᣈӭK|)&&QRR­H|S cG Rɞ}rmZ5gЁv$<6T?Wg\[Bo#>!FЕBD"q ߸RF H 4'55{QѬYfzzb)**bh܋>=}*\Ӽߖ/7;bQ#)wB;2np\]b֝D޾L&cؐx7!'7cOs*WLM  ̴cGn{J%Ne,r!qhf#T XH$ӹ ^9XV -C%5h9"w\nsZZ2ƏF^=xŋ4dhI˖Xf5nBRS104d4u@GG͛73FR{NN<>vd$[IZًv:PVVƤqcqmЀBRSSq&l'E0|*WXP@+χeeffa=ȵyEK L&c闋c߷oz7#~kJo bMٵ;w2tpΜ:E^~>te*j=dggڀ)o~׈pϜ;:t]]]JZj )))dgea[ێn>>V}U',;ߨcOh(.*b&n\-ZdȑH$݋s}Zn_||8~RQc ǏwnLjPgN~ܜ~~\8wBllm=vNUҊ~нgOfzVh_HJL$2 ?N;#̔^}VVx<9|2$Ƀ᜹|4:tD]MzB?!L}.).6#1ad$ *7"33sKKK'pzMOjg٬NNwl݆+K$Ѽ JKJJ.]Jr ǧ{::5SB|Meh P05j؀MP_Zaȭ;vî ˿_H$@ܛ6bL84X,ؼaiؽqs5L0abk}Jߠ#A[[^c~>=9v 2eYz6iDaogdr OAɸJ&qB 3S2}$͢mlܼs"ЮuMSS7UO8Dk|0JEYܿKW4ٓ062u8|cS-GJK˘6eIɕ_ W$ЕFnxw3N4Z4eu҉}p :wjT[y sn3ob|Ԗ4ǎ$"6_w?oHA|>][ ߻6$ nw!9gAu~-A*ЮmKy P(X[֬ۤy si.\¼Y;.^tM5C3Ϧ\ Bm[^ux"X ];wҲr?{R5b̚OX,On1Yj-C*p?:ccœ{Q6}zw'==o~Og qvmnEVUrrXr$ ҧW@TP(XWnmތ&?ydzK3ߙ~L8 Ls`~rss033gΝ\z|ܥ+A7opAMڠ7Ÿ{x;p{w¡^=}1Yp#kٛ7nҭ{ޙ.J+t8\mvofθ{zpN?Ve|ډUreE$V(p2=wơ:khbbՓ͐PlphW??JW}Z#l2bbqvrmO,w|յMrbYa3oMJbǮ},Ī"/?[[ktBmUe6֤g]J@c8;:0B 9JRrmr^WV[~dR^Oz5wF̏SǮBX,˳)3B+(UJR -[xFPp(<*Azӣ Μã 7BPmCII) --J9AapoҘ'2uڱnk?EgD*İ{MO@ɓХKgLLLmڴ7166!$$8h3[ё~R7HMMUH$ޞ1\NRb׮^}HII  U64ufqŵǍի,{:wBPs~4O?\\̆[&&:v:ЫO_u ⬿?C7`|`!66j!ĤVEQc}}=ksbs/]"++3v;ոF:9;^r4$= pWT*E-z] #11ࠛpl*&ceeEB|FS.’DSĐJENNNh>nSuutIKMѱMOfffi;5qqqddd`a"pXŋ~ͽ{w=v"۶q+,F"D(**"ӓѸ> W9773}z GiiY7QXYZqѸFT*M&{{;sz&JFn4lšud쮞/_ۙ7=yTFSLJ.P9$$&a؛h+yϿkbWJw1v0v9o~t? IϨHP#Vi6JU<τ`cmTOBbR2!.3h`߷*2ݽ2|?p:,*LM'X1BBD1tN__aٍlPRQ RҖ#CCԯ͹[fO:mk&V{yGi+L6̟:0SOץZ5kW&6&Gb>hb I #˵b3)5Y#9W66#SaC~$LIyz*:إV1FFZu~1ю)IfαOuD,FFD"mL( bY뛕)$Y뛕9ցX,߿Z!hL&NTъLRÇz%LV^,D" آsnRɩ'XW kLJ,YkkڴmSEU-,-IKM)=%SBʕ=SnLߙ׎$ˆuҭc?regm}m)!!4ilTTG~AcW^Wcdl͛#1 99ej;yyy``hƟqiϞMXؾ |̎I|իh'\|2C8uFܙnٌsaӶ'=#K ݁(LMJ"j"ƭ*,/ ķc[Z4k ,}}}툍z_#`bJňL1cb1!Jiբ -fs=*8:PŹGco/>6-v|[RŹkLaW3y8/XcѿoOLL >~m[ѻ4f.UX|)͚N}FF&zzzӶ߸egd`u511 $R3NjJYehѬ1̘:m Io~8u<{aog˨Ҡz%@Ffr92T+vl߆3.O?/!~k+K[[k*8:pCi;ęs!j~q%-**x4K5Wo 55bBj)E„ mezъgǶmdg`b|1|>(2R /(2"{ 7|ӈpLLMnȥY+="\j5"򹖘*4sJb1add1)tryMG,sYL&#LRmhڼy`0$z~=yW1eڷT^/}^mwR:ñB]% u&"#˜T*OQQa/s~>qjs`>:viYY]Lv5iH$GϞ|#qKJJ̩S p$r\UQy+J/##c2Ǹ'-5#ccרO{[,цukĽvm_x^on,bbbB^^V8T*cf.'dxG)DQ4o*w®\o?y'ϰcj/~狱L9Wkp>$UHMK'66wRՙkrrrrЖEуu&k>2jP~-b={jtN~AFm[ *1&F>cjA$Ѭi#D" =00(QD<`.(1$| Hf;mʗ=\GzV:s{ҽ/ )s0qXԯR$3+@)55?SR:58a,6l-302zi'N%Qsg~'v+6QC222dddҴ;y(J]@MMM8”5bKUgL.//M[v0K,g8:/ٶm={toqup;nՊuo >EFKOSW,23^qqZa$+_1r9vW61r\GGXЖk, u\v_ҨV7_#--\:DFFU]rbcbP*>y疖ܹ}i0ۘ 70@*22)**ڕo-IR\rEm~`$ N QyM?~}r%H$r|2^^^\t ///222prr"77{Dbb"(Jqrrz-k1>ݷoiq9E-ٻOCtϞŵ-X;s6T_W֙uG6ʌ#ܵ ǎaߞ=it&Mf׎,ZĄ,hݶqR}ߘ:}`Rre\\\Yf ";{{wssc޹#ccm_:nͶ[K+KOW{ԉ{rME4oęӧ?Jtҵ|/R T͛166qf\8U8 dff wuףNztLЁ\ ATjO?N5&3#WWN8A^^JΎZqfjD"йk7CVfvtV|Zwn-X6֕5Yjv'I{vb],u!t|,,r_?ߺlndL16.cٙ:vǦTP%Kp5*WLn8|0j͛i&͍qss#00!C̓pww}Ar{й?Ǒ3f6lΎ|ME'GZhJn@%ӡ]+<QQ;{:jЬug . C_&cOrtÀo y- ,@OWJk }T;ci ͚7)/p1$[7oݨ1vvx1,YG1aMGNv6111VF~~>:X .%;Xv&rs4xrs-0hi{I&M033CVӲeKD"6.SJJ Һukd|aiiJ{??ZЧ`anNZzaW/ B?+˗X,&*1'NU@?D"y<%0L ")faҙBcYaH@}õZ5ìQرMޥR^36 {`$d/g3^-5#+=ѓH8}D%HR$~A:| ]1c4/>yЈT*1U@@D& _+[hRf\Wb(39eJ>ɻBaB. ]Yz@_вPy/A"h,{ܥ%k-7dQtٴjUfh_I@@@@@"F&t06;&LZ|aay9eeKo y- ݭ0SMk}5͜il.X/ "Bo1Dz3!I@@@@@@C lG:uoD"&H$bӳ(((jV@@@@@@@@ħCKEZDB!^f}'1rdb|dР.de/yikkɼy_p:ŋ'z>O}z 4+"|eX,fYԮZ6S比S\_fm /}W+@ժN|+d7o tm_pww&7+[25oހ3goY1m(iӡ322`ժYlف(Je)9*L&ƍj3BR=iOƃPT "#c^mӓr|ڵkāۜ&55o\xcf4Ÿ+Ju[g;֭sN]8s gC[ƍ2p`g F~_R+K"ΝJfڵkđ#;wnI˖}E߾Mmvm_ӷm덗W-LLY͛4i!NNL'J#IKdaiϘ1 :[uVSO>})_}5L a 0Qpxߧɸ/>&wk. @Æ7nժUĈDfWrt%))[{qCn>##._Eh*{ǎMؿ׹ĈT|}GBaLQssb;wnΝZW[3hPW֬[>O׮ : XjXr7|ІۏtN[/H[!G^,Yػ$11 %ݾJ[dddc` ]F=zGؼ9][ =GLլ,T3܊n NoýBcDUy^[d:zZ >}:g KMY5),,Β5ٺuv_xx4rR^ml,y | H:;:4!99<"vvxxT;wiF!kE2Ӻ >kksiV*Ue^=%%LM_ZΒcccԻ0x ԗzNlg _[ȀYX(xbWܥ}l81kNpn]HJJ@RvSRuD5< 쭇W-Ltitm; P(= Ӽy}ڶfƌߑHOJJRRʫXOFWS(LEdff4iBL5s$1#ҹK'lmm xxLY[k^իh&== 23s'0p^^97ff&0v@ C8u*VSR}ԇ1cL&-5=z.]ZdBa—_`ggBaLff9ҫW{RitSS#~3ڵ.cΘat SS#NpW/ϮKmMꕑH$|I ymQ.88vP##zj/2Q#k &Wd:q"c07@$tԘA& `*& e΅D"_G$++O3>ʌ[2mR;y2={ѥ˘^,Y'N\bƃhڋC͇?~m4ʕgef^*Vc92;yWI?UgȐ#}>OL|w0Fy |?F^Ź~}nOg.IN$e.ZSGڈRyD"9t?[Jb,c9\\=r(bZ}fL6 ZЗZlX[ 'O)]144MPT 9}-}!^*CѼY#&LFjg ?CXr?>O5ת kkK jՙ0qbHׯ]Z4k⭕{mxm_C8*U>wZ/cy}XYYQC&QXXȈCtҘ!HR}q:xÆR9Ν9#ܜuѳOm?:yM;g6:l8"M6EUw3Ta,>|o xp0ڴf޽<ΞN]P b1_Zٽ AėVvDrgH*UD?~L*E,onL6lKFF6r qppR\~LMxQ+f;͙ATcmʭ;w}VlYNe˟MCDd4͚6bW=Ge_03\VU2%qٍӈb?\ fqOX~+4/=waڌ9ܽj.̚1\6n١ٸ6Mؼn9obϾCW,pVl~&2=c3(,,1ILJYo̜>SѮVȤ2 DžiǯKAA! q$vGb1} [z۷aԈ!L2V-Ҵqj0d@7iL&{ier LñX~uGjݱz#yedt۶qN.s6v3O {ӧhҬ[~aZ232nwFujS-,,JXׯ]Ғ{r$ F&/T62yegHH,P.Ȁ4 A-1s!.U+ҤsfI?VJňjEE:y>6֚Aw/(`|=u"*:9?Q {ء II)_:Ѓ?q* IHR,Ar"x0 !,n߽ǒ?P(hӱ͛6fsٰy;?~G6-iڪ+WdWZ nOܸy[w50SSIKO?Vh7gd0ǟڹ#̜YtKGў!q#IkmQcڴjSG*:9beeH֬LjjNzOEEdΦ(|*hѤRGV >~~=y6{vSD?a|_qu[Я̙mO4kMOIMMcUܹ{#= qu }$. +{ͦ|MMLx尫:s;?uYT: LÆ^&;LVn^.EZCnE|wŸӹk7^ ##=&͚ѷ$ {vµZu7mʜY8:>>>!C vm߁9Up<8y,X zRILL}$%%aaiAGNّā{Q)‚ZSrekÃJu3-AZ4=߻cGC\.vnƎm`2h׾۬\yy4kђ;w0ÑhՊsgΰs6177khժgHo©'էOLjcRSS|'+55Uk8:C%]uJW@KHH̜=G{1|?c:GI{Rd…(9~ES7`defLWC<<=ٻ{Zmƍ4kGGR9M5f-2ː}B.;{{Xr%c>Xy=~^} h,%;oe6xLƐa<~z֕O?N4nڌ#HhH|}&' ?j5jЮ}􈎊"ArqXN UǏ9v(C4q=|!CtĄ'$k+KllV5iw΍Qhr/X>?ϝ~n :r0r`zvsuӵsG~wSXz@ΰ!ppc˶]%4?=3g/0I;Ř#H$4o֘61oŴjѴXeދq!$\lX;(ΝH]qpsFl\V`zƙs!LR@5pU3B^yy؉^Wr:oڴx+Ӿg u0yݠ>d wcԙ_|14 ^7gͷ~?Zvx0̤֮3ٵ{?~}M߹{&ӓТf~6&OR9?b ,Ӿc=Ӿgp5  rE*8: IsA:8WĤ3n<:km~VZЗ>0EG@,ӭsG &NeݳU+X(0S(024ƚ'ݺSaB|}ڡ05v h,0+Tp死:O"HZ2:p14 JRdw?prYYG~mM4L5BZGYgy:xO/*U8|23~M6%33U5VcU88:P˽6'I}oRˠRضe3dZW1wpd/ZՅS'NTµ2e/;qquK׮\zU{%qQ<z#gő͒_6|ʟ~А/@^@,RԺ$'gNvK iӮ}yؿF|x*89qtHwoյV`zZs KfҷCBRRУg/,,5c o\\]5cqb忳{j׭èӬy vnVlx@=߸zpKn BTCӯ?#F"B/]"/77Ǘ1}{2_ BGN|23T*aH$ p$}ٞ\uɓH$޽K&wH}4"{>7n+ɩilڼn]sLWƍ=(*Gҧ=Y~ :0S.^љţP0h@_d`?s5{{Qɑ _OC.ӡ]k~ѭ_gǮ}|9i:_|1݈~CPF pZPܪsA85bnEƘ),}9NA*2qX5mDffzԔGӞm;j٘ıknܦƍYܚHĉSg8{>Np\HEi^WήCܼ8&Wy[/ xHQc\]i޴/m|WdffQrE5m\.'*1WVٺ}XIdfeh 96o/ $&%d\:TIbXOn޺}=THHJƯIMM'Ϝ'TJ,\RRR% o&&dfekUg^}C.'#3_мicV^TOgZ 124ўxWTs\IW,٠.~1 Rɹ QUIhȋ\IAA! \v/ =뿗îҠ~~:]Z|´yN P*dRT*%r^I.>;D:~vnvrANXicGxV&ޜkX"yY`u!m5iB޽Q#rv;wǍ IDAT\z|N:Z&`ć1Wi+ӷ_n) joTΞ%>.}HLzDTws[HO+:_ٳD"\.Gn`@JrJBz166bӆ $$x7ӿ?b(8xf7_nБȈ>ջܖLeQ&Tuq!>>۶'c3ݺrn`pPXXHzz&:[[klm}^h]j6SŊNs) * Z5Vktռ.jL8=*8h*=#8lh=cރ?eY4iMajJRrNT ̀+ZUr)4旺lߵF k/NoܼSȔCSa!-=㩀ٷvFX,fպ1۰=m{1qOt͋.:c>@7&H$A:,[3N~@qO.~J+P͵*c;VQildO6Ld26651u""GQlڲ%]Fn]|ؾ >zXVlIHLJ&e VEpYЀs.X%_ y:BBD1tN__aٍlPRyPjk++zcllLLlJJ[~GG iP6_*SP3of̛=w/MG:uޚ5되)U^ib4ͳ< TvvLČ&--/&+!L9WΝ9Cbfrt@\l,"#uݲ0[PZSS嘙irVzViߡc[YY?}hI EJںо{ ʩ'KR=1kuk2"/lnܼMMs B_Pkױ׼`尫05%;'^E;ywt Yڶĩ3mCӦus>?Դtn߹[jXA2á4^+k` \L&Պ۷̹ deeKHKO EW[[k*8:pCi;ęs!j~q%-**x4K5Wo 55^rJ*v6eL>ZҧO/j֬^K_y`ggҘ5TaJAAIIIXYYyBNv6+ nw.aCuD&#cb+fff`<6u}ePEwQo].v'Xm@)PPBwB$!~Ǒ#i Byii(JM۔@S+ޜ<~,ظ~o'G,T~Uoo~5]]]>klTHD2eq%Z͛S9|Q|ALfxÉCf`+۪]c(JBC<ڽs=8:#.>pP_dd(z&i|3+Zle 8y0duf>D,s츧&By<{`DdժVf_| s6aa8ZZZ}Di104@WOr B(К})J/gWW=|@||7<{Đ}z:?㑟?f9<{ 7o0b@p1093`}7'Ok&7oеS;tYa GOk^ҳ[g\]w/3 T*cWDFFr,0n] *h''~ Ll߹:bkp5Ы{g,9y[yOa"ZZZv3 th׊?NGaޣJbޙCqW6nAZZsgOƊh;] !4kҐ9|j f͸l^c##^z)??sgKWĹx*mZ6MҤKgR͙$Oa] ߛsfБSLbP(Xj=}{wCOJj*ޘgy{[۶qOrS.W9w{׎P aurQ#0sB&ODŽ3ЯG \|g'͙B{\ytalݾ{y$$&z&(`dffqL{lфc'N ۳#"--x*Ca<|ǐ}4֮B.4Va+XOJj'==m[VsIY.EvL@ݎ Фдy n\NRRtNzŊ_ÃN]rYbb].W=֣'Oܙ3ӵGbSe8;|@< XV[ AFrRzuR뛘0j8'[>'OhʩZ<~t97/|`ck}bi٪5{uNNL2=<4thvsYN; ;v={bmm;= @&ýZu&|?R\9Zjͺ51kGs.vrRn]v|pv8~\T[W>Щ3ҹk<..ݺmflll .ؔG[͟a` Nܼq2eQvmN:mĭR\$wuة3Gfaؕ,IڱX;Ѵ;@_&=ƩDEFH$I@UܫRJUZkשSܾu eV:w|"./]w߾̊S/iظ1{v@.Lu$ :tc$'%cmcCy7HiԸ Ǐeұs {x} *Vr#*"ՕmrV SS3:vY?"3j,a/Bܓ{Cy4gMd4Ƈs!% Jb7zg-4k֌FfúXZْRBKXK>::zdd)i_ےi*.wmB__v|p96{A=WqzZ$?-7V<}ׯCX4on]ߤf_4u0WtxntMY]kb⎺8~pOpOrLJ~]ף[X%ݺGԪ]kkV;/Xv?s"kΟ;#ժWȡCx:3p-_^hTr X2 \HINZ*NZjTJ,,Xh)[89T)Q*U2L22RjiHlqmܸRc:f'${!:ڂܻn]:i b1C8'\H$ԯózHN_MMsW*qKV?=(}^0qT4) L'gg.?ς_Rbwb-6~d[AdZ2Te}Y$)^*O^+iI DFajjFjJRmm22ؗ%55x$bӱ#<<3Kte%SZhI$\vS_R--yORٍ"ݣ#ݻwþϓ0jZzдa΄.6 Cy 4>g>L>_S"=-9K@@@KEC<N"-)Yd) ߅SN)6*2[#ɐgquv&19pHKKb| ----  |oI@@@@btYesVI sW7Q܎ t-"Վjer$@m4.`Ҽo!ggSS@GWXL.>G,BOO++ >yH,B52>D&0CѮ}[< [N8eU4)rm &'I@ 8;;P166E,ԄT026]m)anX :DDE' PWҲe LLL("~T~Mll)$  qF " L / PnC*UB*&$۴٥> A@@@ϭ|2 |2 Q#v" LVV+W""bRB~SB|4p`pKl-RIӷo[J%"`&oYUSo\x"͚BRO||g`u?Q+fhi >>xT"H%VAή]'Yx 66ڵ ++sttLgϮgŴoP][PT|/hkא)S!gΥK>hQ˗}M,73b g\g߾Ӛ;6"< c: *8PnNŋ7̚CCMs <= DN̙ؔ9sCSVo׮!8i3 Ё?LL 4i0gf= 8q2-OT*ߦ!Ҍ3pvt\s~ɶm XM<{Zs<Nʸ? FOOݻObiُm摔¶mGٷϫT^֭[Z3`̚5`FÅ?ttT=>Cγpw9&&e˪;w++3>φ )[֎ qy+=*cmJE\\"#GLTT%KZPdÇO=-lg11X[feeFTT XX"*ΒΟ?\,^sQzE/12{<&&r )QCCO_o.C"Md,)b>]XHD"* 33S:oӧO GGGkի( ڷoO͚5ILĽǏD &55ssstlÎ}Gդ- ށΝ; 'VL PtMie[IvLCo8%U@(Qzbm]6mPvetiJժkfԬY6m`m]Qz9]cc:uj̓'/HNNa"ׯqުU=)11dee9s7?/::[eJ*0^~DtHD610Ĺs7i߾V2iKC5\TرmDhkkxܹ ҉%L12`74o^ {{k6msiRSIII*Y`Fe6%[ke@R)*TjժԬY2e`ccCϞ=С 9s///iԨ t8::ҠAj׮M͚5}+P}*Byx3tpNV4k]O[[ۗx5c 2}}29z`m[w@ؗcP<[BU-tyY?nkcK}3|ȀO֥RuR֥ =4:oCo}B\?QvRtǽJ%<903O@ ?{ o_ޭܿ}ާc",J7hҸ>wmޭxk?vp'2xJkߵ[Wp^'gLᡏ7es߳}=MreKӏ;^ٵc='~W$ԉ,rtHMIa/pяr?qޱeKv߻p޼/9=OzZzD:7LvtoZiav'\8wAOv LyI6rP,\oز%K>xf|h|:XBCٲicuUΞaޱ\ Th̙57KY` ޫ/YfGK/::[lٴϲcܻH$emnIM>m޽etG1slt p&bcy!^ÇC.ٛ<鍋K"v0upN,6m:/MWWW;BVV'.fڴL0p&LXDF VÚ5?!qDd̞֬=%L{O?CϞHJJaٲ턆Fx<,,Rٱc>\pMPdrE֭D"ç!TºXSNiR kyYJU6$&&DRR*U T1666 022B__LT*UhX _VVٳ'kRb6lNFxE% ߴuk` |= ,\muٮu v{MGFz2{WaU(7cG&4S}kYtCѻT%.mDG5bN&!1X;y{Xb "_++gμ%x_ 'c)L;qs(<Ȑ+3nW\UX#߻ѻ7Ϟ>Ì=.ݺ嚘ݿw/]tJ#0rgs&[N1pa |o0zGQ=\7 L`|]`@CK*Z$ 3ppt^n]5"G+VfjݦXq8kWx_Nrr* &8$SgkV\B2bqqܻ8ROv [kk>#99;;ԫm^v9LF/]$!oBQ*TCz&Q4WoǍľσ^` ?xRvl^m;QrEqB.ceeܙ?R3dPޜp3P Ӧ|#)):Ǣ+Ս]KxEݣ ~Œ'RFuc&?nhf†5Qՙ7o3k"RRR^7o|Zl:'kzd ד .rInR{+)/ZhBff&\ԧl[%0DƭK:p_%+Vm[v-m܆G>gGvl^m_gѺe3}/_ucFPdfҿo<ܫpwz͔dz|^ --Ysard$^~Z&1zPɋj_h tjթWYa+?LL%͘:;&N@n4kҐ_Kbqʂ}{%4mvD sd}3_w,-KgN{ҲUkK"33gxjVryptr$?ﯙ$$sIBTA Ywt۷lvݺ89;$'@zDk 9TA_D|+&/5-Shz// g9t?ܫWNlڰ???T*..|5r$FF ח*U޽{XYY1z8mlPT:p a`h< d]cjfFviظ1 Z}qvv)Y*~]yz:~]n>Ϧm9|W>\<dhIj+/saqq-ψ02tiiؗ*Mm"# >.j:ϤzGWǿdG˵u|MD"fϙzׯYrw}|pptS> ,ի>xLMMЩ3 7殏v:r/ ԭ_{kƆ[{8;;`J(kx!:u)}6gА߻WTP~G)FHp0=Oy swD]DFFy$sk3gq۴՜G|/g=.L^Bll,W._c.PTl۲ACg\wr7ryJS.HRܷ}GBBĘAAѨqJ)CzZ'Oki;osM\\Ӹi"ץkWIƍ~ 6no.NܾyDBZ'(:tꌶ6UVs( ksb/ksխy֚ߖBFF+X+C[;kvykkm4Z)ܶ}{pO:"5%^^ן1ǓG&[;7GMv&yAAܹu~2dWy[>Z߄0j8wY//,'JmRs6^"㮫S 244sssr8]]]T*bX}͉ c_Wr]JKmD̚e+VsBy؟aꌹR-Cشu'5=ѺeS:ƣoŎ\;Uk7av~]^ݴ;MG{2or.U[jBCÙ1{>.# )MC,.ʵҗbffQ1zҽJ%vO\\<z"Wn-])hTk7}>~e1ϜǤAGYd w,igE s֮Yf#?[+Βe3ܽDҢ}|Q*Tr+O.<z #ʻzzԩU:w Hҩ&Ϝ/ ~%3Z6Ws!1餤rL^MjU8tTs } RɓIIMF˴na[w!:&ߕiQ kHHLdԉ( /^^h:)!&6'{iYY[6Ν~ϣsۣ6DE>եnxz#++3g/кU//3zꍍm 0 "HV[GS.RSSS+0.I N..͚Ӽe+P(D"8::S yZXֈfmWѻ(11/^Pr/ſ{X"eNKJ*̝jըנ?LFu161~+i-ף0rht􈌌T/#H=n=z&בtރ-[1lHuBky LNj˱-[лo_jשR$/ JjAo]~W/_Rn]w_affLi&ce%fED޽iѪ ~W tfT\{w5"F[[$ 5&?@kkkptr"=݂1o]E3__/?η|| 1%KR$12uҵ{wFo!p2zxƌDBT>KMM=::rKKKlI֭177¹—E\N-䉊OR¢6sI@yr9Ξ{^Jn\pA^&$Ҡa#t061{5?׌%0x0n*s岺n[ZZҨqʖ+Ǡ!CӿA"L9uk UDuߟp[LAC2hP^IM]Gg\ nݼI2e,hp IDATٻ7=ji>HH$D!H0x0UݫBDbTH$¥|y(B*U142BGGޗbKBOOEFFs7m;>x{_is<.HjkidSJRDOO===d2Τt|STՃلķbڂB.u[)NhzT*INJAc"c~F X,fνujeIo]'&,@_5=?;&&lپ-wkg_PřaNv=c6^Nu```<"Zr&W,svي"_4-eccͼ#3ig߷ n\162{TJܫTѡ,7mXkӒvmޙۗ|'<}Wn~8;bnfJdT4.Nd:ڌ=AF[[w_bbc151β#?`qB8yairX_~"r 2-Q ۓsy&tƵkܾuFMP{;nVIK Jn[r:.*qp̱RPc6B߼.ϹٓJX]]]sq1j lK8KKKd&&[KK~$EmQl\؝۷|N_&&LJD 500l!g-"5B 3S2Z}kdaem B& qX 3gr 69GO_>>x_NL)[V"]NRTZ1blL |;vL.1DT(hݚϳjJ,,-hڬfһ\3kw^˔aqo֮aSdDIwf"}[ZZdj$9vtrҴ9-XyFFDF`ԫy~6vcbb ΝC$߷Ԫ][d2OIRu8q8e˕eV>dOoQv], _zی [*/ܶ.ݺWطg#Gw 2 |rE&]]]6l؀I3M4a1mbggGJJ Jhpơ5a8s! Q* ܟ磯DKǁ#ϞQE=,5\]导3 ?fYXQكjT3Oi٬1:ndl93hөWizWpt,_JRli^%~5tm]+ .zrazBB"ElLI۝&~7 [k033-#gp *W} 㐈 pmlHH'66HJ9LzpTHĚUk&weʖ%00ׯ9Dϝ%M(GXO?B:t)),w230~\042˗!![Ssu~6DEE|NLxcXAqE$JL7o2 y2bcdpE;7˅{L+P~}^|!$0gҽgOwrYz7vttfaaiӦi,)JzMff&wy$_:yήܱ<._{w+uڕӧqT.MeR ,\X(DxȮ\}kkk%"ErSm&% %-5g~ZL5DzZ))+ ~]1addLffr _<+] % |ϫ`pE濖H$zkرcYj˗/gԩ˂ 0`ׯG"`ffH$BT~/>gϫR?MQhX{P uS6TZZ5kEd<ߍEVV^E̾3MbquqI/J%v69i?L`Řu<zɈas660m}|йc[$ vv6ԪY{nO 2^$;gh\2-.&Ѐ3"yEutq].T bCAłc]c{Ԙ[{{ذPYXvca%{g;wvsބ+_釉1ii(bct]F DBk+W}s}oSXa+LhkkSF5L{10dzT^GkXkei˔A,chh{U3ӓоmKѣ;瘝=׮\ATWqtvd2RRRضy3)))%NN8z*E,DjϥXxJ޺M.]K$rrzr&&&l&VM_dnxy HII&33b5j"Yoܿٳ_ƍ?uV-׮qQ6mXObb"|DզF&Q\9ذn-֬I~L~=eܹk׎={ H044Bӹ Xv՜ng߫S'ŊYn3 Mgbk?xzĩzQ ƙsX[[ҩC[ΜȂſ/rEڷʥ}n <|BHO`܄i$$&Rw`s<D·FS W38u2/w=PLLJU177[/H׬|:}rsedeeGjG&T*0{BC3c7e\> ;̹] DBjx&=uba= ͊\H$,?dou)+q1JC(W.` %黊pOБpMosUd<̞Μ3|_͢L=Cl^;ѱ|%^ӳgQp imwεǙ3{&« aaOX]JoϛMsٳ}ԾwH$bw*h٠DݒxtS1c >>o_iۖ8.ZKܵx5Sn+~[ζ͛г{_qrrg8:1Xr1qu1wNSxqqqd| U/*TٍLTn@79oA2>7z郮.K-$ܽ{ öѨIBCCz ZT-˄ɓ70`׎$''1x>yLH9|wiݦ-zxczX#Inݩ`cdq]~6mXYysLQ$n2ehӮeK,k^i%]l]&til/Mo_*~sܿ'aatؑÇQbU]77\Ç;q=-^LjJz|QFFƬm9׬QI]==vK-st]NDeʺ*r;(]Km\f,2~'w(?̞Y~'%>!m;а~=ֳ͛j&{T#`mU/i&ŕs@&|;\O fmKLYnRp:obE; K_eiBEb2 \Lfvng5p3;Sq(J455y1<@P J+b1ܹ;ar444x< S[7rrs ca|5r(2O: u*P<6 /} ˹{*_ LenR) ;ȨJoeA^JnRIނ 2 ,HeMe4y739J$~E"999šT*155ҲSv6ldgg\d%.>_M@[[--MOš#'sԴw'ٻ0deem6 6Fw\pPY҆IWr&Zv6ieAuAU_,U7T*Db/" t ho (VLor;{,r͉eɒ%4lؐ#GҨQ#444 //mmmMr՝8//6@IĔVV+넵j YH Tu^JUAh79!X2  \L&x,$(ì} ݻSNPJ:wcǎEhii6bBQUQ*Aý \G)S"~-᧊M[ݤZ8R X2 -^!>Fԑa}ceyJ8A1 0/__ 66oڴ[.T^]I? 2eDjwECCCI@@@s]tIfL'|@─EvtGGG2I@@SZ%5SK+kBC"Jpp,T'Dp" ?ҫ|ѱvvvBe L5A]*C@@@@@@@_D>KA.*C@@@@@@@@>tt<۷yyB>!I@@@@@@GGGЦmʔ |"'>þ@˖ޘ " #L2 10D mW2VG4i\L*I@@ScC 量OKLLL [_D ؘj&i`VNB% 2e]lbn^l/ o,wᶩTs\.%L*g^nfg !G9R 156d!nP! O_)S"KLWݤZ4R"XgsT#Ԭȧ/257de4*%c&,Vr@6iP!`SZYSzfȊFb0R &R^"yYNB@}Μ'11CӨ+˖m#"EexycԨ9i!Kl[+o߶L8BH$".. _ߛ,^;Lv hm4B$qe-|"S56K-0 GInѤTHDy$,MȒ*"&DX":(A]]/SxQ浃C8;{: PYu1܅K.>DEP122d֌ITsr$8$Y@g4U{5f;p'3 ע_}PJ~se:L2s̈́ hkk:ODx8!""mm:8Уw۱U5_ӿcBCCtt>?:y#G}8yqqq)[V`ieճd̬ VVfX˗ ##}*U8ɓWٷ?RgPn/\]{2l؏X[3gcQ N_㴶.;gWT&MDL*gD"\,S13źL.edXQȨP63WLeXͥa.)hkM@||+Vo 0ն71۵nڲ7bVHjZqqdggDzyTaŚ XZv44^uZEt w=` ŗxҦTq\~?cӦU\f#j`U-S1Zb *`6FF ;3Y嫐:}jN,_+;vyN<Sc~YxQ(j{R)>aldÊW݋^'r\.}߷_Шy[&Mҫ{0m7%}Z4%ʔ-Kmsiii\賐>\aTT|CS//pSyNOZ`U- {K8|# ̼/夰%f]AvFѫ;EgyoЀ 1kVӿ#69?$(NWS ]Ʊ#e_\C5[ jɓ'9y*OFWYcCض(;we-k’%[Y`aaOرb6`( ڴiر}P(8q7 <++3fArv>ɪU{z=ϟw8qb%+[e˦ТŗX2y a3޽Я_{J%glvJ%̜9zjemD"ѡOgsqFIǎT %~m' %K&qr g?9}3|47|k pգ;)Q"rhrIL-Mڒhje赿VKrC}%E1u8zJνOH$Ocwfo5nAAn꒖Ί8t«L֭ZF5'nµ ~7Xj,-9}l/8q\vG=HfV'-/'NӪe38u悺=` E:0nmF&}Fļˋ+N۶nI}7֬߂[o|6l۸:k}Z0vT.^Z|7#ˣ_/^.n 2qKًdg[}==2IKKً4j}DD&1r:wlǮ=lNgYom7X,ʥMMMaMq zQqRɩ' 6%99YmtM^KR`C֭142Bcx1>ZcckK^^gϜ&,$}=ÔH~E4;۶x6"DoЀ^}ɑCHOOu}jݚ3NIRϞ9͡0-cJ*Ux< ,̌;wrAWZ4nxrR9‹(tiG'"閍?` 6TH9ھo&L,CܺyfD==|[[\j!3#m[ж]{llmi Yo^%**ݻzYiήoserrrX;n8zcc"1-S^ͨXi7npzNx5o^׮\!//&^^gq\xyl"x6F?{W/_'>.;Cڵp2*D"VVf>3F=zyY,u]N':wq2fLoURTД}ң$Z^X[g„5>}и:ȑ=e =w\PÇOpr\Xg2`w4hB߾2{zD͚UׯӦ}IllؼKL>;v?O}1ڵkJӦu{2=zLã&:5W{3G#ekJg2)iNyd#K,=+l'=+LXzvY9r2ȒʉOcaD#T1 =8z &N5QRi!"~7ҩ}4lB!k8;Ve!:)2F ̈/0E( W!Ix!Wɞ:k gM~S6Di٢XILL4jwS-,N3~(85XXEL 1j! O++ -\~[e˰|ќ7ZMvԎ{I8͚6OOŵ ;wiJy~q:6Yjާon4lSz5РJH$9;"˹x_AuczGSpUг}c'݌S0b̷_J'^^gd|;e&+l_z̤i?~vZd(q1+-t|߁#ޙm+NKlȁCpws{׎x7"֩EJ6s6?YAGG9s޷)t=X ڒsҕU L\ĩiՂҾ}+w*UX,,`b1j4z@IIIE C6mj( o3noЀ}{DPpe23:w牉.ŝ5idIXYWPr&&|5kUJ}\\s1rhu?/JIIN²5Kusl_Uny 9pR)WΌqM|Zƻwr1uڻwihPNFFr`^ЁTR׺n\}$''p\RSRֳ'Ƭ_ӄ%W.]b֭󨏦&;m#'p=@Jr=ڵoOMN?Ɓ} ,;wlƆuR%L:ccRՅsps@"sbcb eUXY[ӥ{wBCCXzu)))<~/:vbߠ˙ӧ帒v|\y oj=sشܿwyڶy3VVTQ yn޸cGqrv{^Ʋ}떒׮bbj7&Ь7g_HNNƣA"ǚ6kƣ Μ>#G:}z}e?j\-"y~[ r۷lam=|HJmصc6oid[ϽnjN?Mv ׯS޹];v}˖"<|p'өK444HKKUϦ /T׏s`^6o>??uǏS!+3Oӫo?7,>_$]ԋ(,7[ۛpn^o r/^pv/5f,;|ӧWۊuܼq++ON`wdHtryswã8r"͚c`H$[4lB``0::̟?s\ )%ݺ'6"##,ƏOZZftOPݾCC}s?O+yTŋ-E&#됗'@S@RR*ffE[,[P(i׮ Dz w`ǎx{קidy;v BQel|Dն6s߯r}e{6xc91ul444hē TϞGavFRd%WѢyfN@nn..XZb9ammIpF\Tp0'/ONZ-7R::`Ǯ}U$>!4.5)4/F< Ϗ*}l`cÄɪgWkkk,,T}ժ$$ģP( oESS5j"ʷ}>;wA[[SSSV%ѣ7Zppt"$$nnPѱ߼7::hjj"Dbo_+.ӓP튈jkOWW_{JdDzA9(E {X"+ĖS2>\\\=v56ͽ&&ܿ/_IVbg_P(H\jy%455=v,ܺQGNN]uaFԮäp'jMAu&˗,ۧ%;wAS'IOO/$R<4n\;xHxyܿ{˾ 'b8ý~Daar [[3mPLM {F^޻F''P ԭL֞lYcdff??qrLMP_Hz~>,>Blw-\~sj&'hP[ -fҾ. _ˤC#}b .fC9ggӐ*VD'%jsgdf̦x֖YT\2B #=&#vahhhyn <x;D%} ?vSjh:ybbbLRԡ-os0 η~g ؅yJx+r-u!R5 UY*--- QP59aldwP*q]enت(jƇvm^-,m*Z Q0-cJ\|NUYpbG~utJLLJjTTkĢJ^rNBR^7oȖd*ƒ봴F*p/bJ,'$1\c~p{z2--M Mm[{sulA_QXц ~Wjw%&%f\WWBpwwE!f ܣrWbhhH޽?zɊߖEns ˏ~cG#J^&[xIgf^^w*+3BjkT*IKK[ojոpu AÆN:IvVʫ۠mŊ#&4$5UT*vhڬY7Q;Y]M~2>_sdl۲/.vA_ 0ꪭG㰰D"Ũ)9Q5G-X/_}_o&&†Z,R`U b7o /-WN| ?15cz/cc -33206~5NDPE IB͞YYHRuhhhл_7ꪅ8WfqUE&mmmΜ:;6###֣{֭[ݕ3#99 Ɉ%-`ҔDDj 9ߕkW.#q\bCCvAUzUq bccejiio`X!餦P*<{sgμE._Ғ(322ܺyBAVffĪ8oQYr7>mmmu耿59J*UiݪH{115A'^ Hi.=}׬ [>z|\oL]Os.FUg!2caaƯ~MRS3hؕGѱMH4lBpp$9hjj8!OLL̔P]b'UuiǕ ɡ[s}vJc@S>3"))X18:ؿwYV[}93==]44IMK+ўOqvTMccKg%U !ի9aee^[-`ܭnmUO;^R%^ ټu;ԭRjaWY=pb&;V#ۋ_~|5yq~}z`jjš[ЕHء _Ib[WguV2eȕrrx:€ IDATL&#!1:.5Yb"=g)<'7Yn.&F$ׯZQMJw1P8q,t]wl2tBwTh'a+=~K sF kZܸXh2O /8jdl̯gҦc'OsQp(R"1ܳA]7<Ǝb 4f-ZǵG8,xr &&== ڵiب:LL߾[99'SV-tݧzƛ KKRSSHJJ"!> 66DÇӓ5k!XjIʕ g|ѩDϝ%E4_^>7yO'/OηƖnodHrrrDEz9`jdDVVǓA2o1'2YDx8FFj 7shHDVvZHOO+չUXq䐓#-70 ) civ6 DRl\_*xIJPolܸ`dlDnn. +WY\xw,4F?3-}hզ-Ϟ>ePh1lffX&]xgggbV- Y˅nnlXHwZujm&ʛ[ԽQ;`DHD~r2Ǐc.;{+ {xmoWK[G'g{{}T*Ŭ|yBvVgϜڵ"s'8NQ۵ZtfgaN&x;##c Q _obcbH$xu+UlDEEk)?`i; ̜'7Wfڷo3fƺu Y򼉳g8#zz*UGWΟ*K׮&&&T*O ڥҌ_~=""^ܹHr3wx{{ qr ÆuECC-0thg22T1 :s0Ɔ3hPGΟfFQbeyx:JEʸ$ū12ʗѢ}{̴v? "R]1c)mB{u1V\D"'1fP4j^U~|ՏGOұC\ký:X|5r(fQrN>WTfIL8'Ǫ;~Z-6WsVu?xċ1 ԗ12ˁDr}<zpxL.\B.3z`zʏ3жZp) ϣ^V-1lHfNj tÇLBM> &(8{HRDLK< Go cxZw,-5c2'Ɂ'ϢP(htu%ܻN0|yV=:}MMM-p˝{K}'Q"7"A_/Z^h/NߕI~f3미~B./6F?n>͐H$Tbޜ|՟7ﰘ̓ [Y6iEZjLfF2-4$HB]x-occCXXzܻy1FFYٱu qqq}qkHOGԛGkow͕vm|tŏ'Qϕ.ӦU"pU1gtdZ 6gRzBnFv:<+WSjTQSmiۖǎtB ܵx5kΙS'Yr455гNG=@G,:8v`gg;([`wU:*`Ab%5bby7oz1=41Ǝ`]AHRwX])&s_03̙S~y+r'>e3/oopvv~`?q3E-v;SQd ܾzBII1Ο'7'.aa,ZVuXZY17[xbHUl߶ LA3n=ӄv'رm=z1cqO"00?{AL:(N?MkdW'T aMJ]\8hP3HH~RlehվI6oX$fȰz1$g%,cGt+2))߷NxR]ݽa?w\F}ަ}@^x%6""B'-YrOb- K SoM^sn6mw>x$~!-7%O>x_߭L3z([4c/S[[>cQtQ"G`'deY$= 7%0N8\@Ιӧr9]b`/zj*+ozտǎQ[[czsUm@CmTZZ?la SS~uii:  \.㧟apZHR.]Ja“[rU(jluU&}: cJhШH MљEZEzE1 R)=_QĘCeƏpk{IIIf{EGkټt~%C+4LJLE๸: /?:ju 2 {LsB-f؊I@@@"K4n-؉Pk}%'47U579!L@",^*>*GY0 ?v8cƌE( ʳYҴ&h4A@3D& tpcS$7@&- *+%?i1 !tQ"' RzMleYm`$ BIȣ%2 H$bfNȐ D@@@@@@@@ E ЀS3lPl[ " D&& ΙA~} D@@@@@@@@ LL5s % 2 ñ47eƌI sss@I@@@@@9sfMo߾ " "L@6B!(Թ:  E˻P "+`Z-<kFkqK@@CkMD^=122 D@@@@@@@@OEdxvctL&_ 2 Ap`$P KI@/&ؔ&fɍ7}yu\PV]QƁ[$W7VM.Ό1jZX"E*kr),.e`nh5j;^N(kjF֧Ua(;6%n=zD"@N%~|vrK8v"'#[ 8SȞPJ$I@@@@@"?'ޥmۨ2!z,vĀFֶ ehF5!/,ȱ E=bei W/F$!jZ.έs~z!{>k~\wչSGLwr۰n=қ7歑^N1tK?乾t!kޡ0nyn" GrR6Jzz:2CAr # Tj(ÌY3sl,3LםFʶU+;<=k&_>yyާw}?|-d֒7>r;>-OnN{vG1mSyvF{1g.TN2l>Oii Q;؈^a \RXH˯Hܠk9O˛_~/iF_OO//=9֯յ&O֧Yf Iv@@d?vrX RoߞQ?w `ffΣVCRBn@~M>]@mC$ '}& "^gzvZӔȈb%EjjA UUJ.]N̘2Zͪu\KC Y:8FF ?{?| p!w5x@_~ٸ{Ũ0~4**LԁAjjZi~7.7?_乚{ߕLpP֮@/>3{粝a[; ^6{;{:;vfYYηkw q.?е>1f8#FݽqǷh멩,Sx:P^^k 0[XUe%|!6nYݠ/<ޱvqAfΞ+|Jdr9}gͩ'y|̘\nem̀ΛTJbb"!mIIIu6㹚[3o.F7q{"N*5?&&,RxfK,p2(JZ-ZDD"J000SSSJjj^h\tw=ƈD*4{{vɨc0h0FF\ 8y,@.?rOJٜy{>wVƒo؈X[uK|<9a"sϧ޾A~+$Wޘ5G!KVՋ>Zk))E'Aŋ2g|j~^GGGPTTľ1w D&b05EˆpcfxsrXBzf>R)PS[KVha#H8~:FZ2ү_88#ɚ<.Íhjjk߇8sfM%˗`U IDATp @USÅK\:w#NPQQE]ao/>{Xg^@II)7&1)'FIbR -`q||%Fw?e]uc+hڕϑe8=112Rؾg]SXT_G)O655ގIeՙ4 7gǎ"?ݺ܂98;q-:ظzǹ8ӷ_fݯݍSgҫoR?oIw0.y?_o^y<"*z~'QORPXĉCIΨqS?vsDBB2K^[DЎ7?-2N'ʂ^'Nw?8uϿ{;wFN! D&c##&O7UUlݱSus?Q N]]{bߧۘt'0ݍa[3ӢĘgOハΚ7UKa !lZX'/|yR){vGg;УgǯX˸'ک23gFP4HcdlLhN>[YQ͛7ww&N̑C"ӥ8>r$W!"͛]g}))=s&~8~!Æ{e7ޝqǣh9m*>TUUOHh(ڵg_V*0ǎ/:]矱_e3ݛ\Bk|kyuK :1ƱqN;Fm]-ݻI7Fêɓ8::2cF- ؽs'XY[1``),,$*2s sbieE^zˣDF3>4"Rh$&$pY:4؟@Tlߺf^رm^t֍7e&xIOyE}gرܱ_njaȰ,~y*++yOԟ:;HAiP'&OJ0GTe]-z^8w cUUY rrusdoo 555r JVK̞hLt"  wq/ݪ7= XLm!0ʊ "֮a𐡸>zѣ$$,۴mK>}5%Ç"055cСzA^Ԑ}{O +3=(-)ƚG`cc w} ]u̩%:u/+9t 5*^>>TJfF{vSUY+ n$HhegOFzF3g8s$Jm6b$9Y$&$2f8@gt`>?5A*8#r\;%%|/^8OͲs 71Sj(TT'~ fO4/S[HfD JJ\K…$239q2eU%y9?rMcݚo޽bLLL6ܜna҂%:+xus\gx|&ѭ+Gɼ93߇SSSә3s4 n\K7ĘQq w*n}# "))¢bƎA>wȨhzk/P_06s;qGf֙#N6hϲOޣ/kk+C]9+\Jx W6ӳG ŝ~g'>.^b1u[BN>^bڶF.ZXv b?p5:o2{Vy7nsw 0/tbĸQKXOLzr,vwmyշa:&<1冼mRS7mhڥʴ&e6lLvhGLn g>I0g<=O1\MI LԔwNAٹg QZullly ?y8ݺ?Jz9⅋ 4 Epy@gRona_K׮l+cGPQ^a1ܿܜFщ2*ujJpt3>z0_,LTVTIիΚ-??nnWyR{V cH}'.(Y2pⴷΣ9?{2HKK#))˗/ajjJyy9FPPdffORRiiideeQQ^ΙSyޥCUe%WSRb2Qt u6޵)xyyÇYv- K9"JEEy9k0pq*/=kWbڵ̚4GHDj5j5:q?==MǮD팤cX^];U\\LvV "3#O?FS~5 6Jyn":y㯨պp YY88:pwȡC H~b먫˗S[SC}ٿo}ߙҩ3r[CpptdwTѻ(**bM ,Ǐ҂-"O_FipLaa!%%%tڵ\|^*&z;vcj`Yscy8 ̝GQTlm#nmXOEGgyYsÃ7yd\̴O1srs@m Lii$3mS̘9yRعc;fȡC͛)oeKmm-[7ouXw+׮n~Y_;ӧNY'"okEEE]? vkԮt҅-ŅG4z\vVsYaZ={xfΞC;{eu5+g̜;y ~$&&'ط1O^ޞӧNNzZn8iѣgO|)+IJr2iixܚ e9{ " џHd֨&%=JΣ:Ǧ-Xo|YWo&2r۷F'ΒMPȍШՈŹw5|9sk+r9#B$ֻ;q|/'wY7ٴen]8z~+Z˧u8zcA=r7K?>Y85hccBxnbaL8s3S~՝-;̬l}Cr7:s_}k=10oܷ{|rH7pvrƚ.ػ +.^?.8tV69wFC_}<&. F ];r V!H5_zn R{Sҿo8".KUUiA m;##3gAБh4SQYU nMvڿ: y߯Llغys1V#Lڴj 033gXYՏVTTD"׵2sTfNʋ=!x v/]K׮H$:u'z9 {{{R)(UJ4 IItv8ZKt KKK<<=IrIIIw}ҷR)D"T55xxxr5ES)%%wbX,{*,,d5׋3QVvi׾#쮔č};*,ҲQkkkjjjB =|\NbB}fgeaie.;ONjUe% #E]vvd2$ ;p5]⤖7!0"#=o_}|6_?_ӛL[m_@~bƍOnݐJ﨔z2''Nd)HRZ-*}{c0v:p@/t%&\aҔL6Ck&.vI.]xr$ Ȏm[~0 3|F B,B!+WokkkKzz}Dfh E lRou&?ro]E899d]\o}cc}}%!?8{"Zpow?[ ԟ! 8|%o/,/(ۓe/E&7d14QzWQq1r jI]I!Du}J-{ʻĂ^F"ҁe˖ajn.{gwCBm9Q*kG>}tq}{cnm-ƚҒFGX^\pJ^ZԔ'|ױ|rmgqgkUo :CCz?~hZٵ3ހNϏ1$ĄDw.--%znm\[eu5I h4V9s=ToghW]S-Wk Σ9=cchh`ggGuu5 EPP]]FHDnn.mڴATR]]{m`dlmoRWJiiN%w ))z];6b$ff( 111ϟ=QHLL__?Ү_͛xxzD"jjjx;砳|]O޶+X[fffNyy92 K jc,leM =ٶeKo _}MacWSwqmݚtp.$4rV N]6X\ƖzenSRRL. "^~SU()z?|CTUVՋ)PQUym?~j_??Qr`>*+GyoWT"];3w*k GKmӮ}{D"7nPQ^o6[tڶuӿ]iZ-z^WWUU=] l[һOFZjaM7ʍ\;lmiRWP`cۊL 033DX,WbKFF $RI>\׭Av~}|}P?Yf1.6Hqvkӆ\t[N^K VIvkx?Lcۋ~]ԙsL0KK a !H8y,E%ɗ+[V[VVA|=fŧedS/HsUM 55X --,"wYl&Q{2iX ׬2ܔ1h%&GF,s zG+C` aݻSTTĶ-IINO!۴SR^7_ϗ5.XܑڴiKA~>99\/]bǶéǙ2m:}g\MIy&&z716z&_TadJUCmm~S'OҾ}{<9aނI{n,‚|]\L۷m%[7 _R?oӶ-IIdfd0|d㱉Knv3gi ^jZ\giBCCbb"nnn իdL<< i׮;w &Nȅ "88m۶ ,mkJ hQzKK+rsQzZ>>_KKKb1kVJ6m(//#jg$謜ڴm͛`С 3l[믭N]c(,,Foa,MLYvwjz[5cRGl<=v,'O zB⵸ҥX2DIk2Ee%}gdۯUoEJ[[[8}tˮ}Ү}:w5{166 Ew&Oih~&TUY^)_?XVv8uM{Ǐ ((^,F|*BB;QWWGfF}BAUU5;ʻDɯ)̙\23INNѣ̘9 TLiv9&EnqyvGb֜yMnq@%%q4 99c_`h`o}21 i̘9 BA8'[6[ЩKW}ٙ r+Vβe7f1̞=>{YyΟ?_ϿG`y KR"5[ @tjhhA$![>ZX< ܛ3s](w6L4#N=r7# "8=;uW<&? Vݼ\/ dh}竛OhV^:sxLFV3ot|b4o/y6^dfer™5c2K^]tgnlB!ىgS:WՓ!34řWIHLý-JKWͽAƍYhc1dAŋŮ{h4,zn K9lBxK]ბH$899йSG.]jܝAƃ!\/g;0~^礩2CC:tR\\͢f}DU̙;FÚ~jz$cv*162B,S[[ฉᗟ9}*|VXA&gLLMP*-1l׮QWWǵWYv X;%'Mjjj7LC"Gj$&\ıdxr~jkk)--%bj r IDATJKmY!!*27*D$&$PRZqmݺYʊ .;G&W8=9D*}dv+Ns;`ddٳg)**bѢEֲl2VXo[g̙3A׮]y9r$seժUeJ%{ںpqm_}F[-JЮqqq2qΜ:Km+;v!*g}V~z J̤۴R?];XjU~Dnk Nj VK\EZRa|KInh>6aÇ׋a!(vlJHvn@mmm-k~ S33&MJ^n.Q;#L^9B]]~֨!SOgͪ8qjt{POg*ɸ%&fgefF4O._GRVmbАl[A̝ s?s_ ÃDͱ!<=zw)tpt:u.4'v:9ٴnFo!WYY ee\\]@+eD"NNNkyyy W((+JBY]MeEŭv䁮yݚ5$%&"12RPLJKqf=NRb"9ٺ0GMdou\]D.c`hJ }}#{)8y唕$EEB-(.ɕ+n Z|7n5" sAYΣc\')1!R#D"NۯJYCFMm''{a߁|*3{VڽW%"R)_}76dK^[x6nnؙۼ5)@T+!+;?3'f!hG̾|w:1n:*wh'L^Xf§IJ{|֢VWûoƌٴuR_p6oOX7~m+.^:Rcv+ qAdɫxE鲯e} Wՙ}yert cXMD 2 >yfs[^|@WlZʨ+Q-t}['&&crs߻.0q2'C`]¸|9r tm\h "3#oVD*_¶^ۛg.dƍݷ(r:]ٵ#w6Co^IqQA?v. wd6\yE0fΞ_^όYٿw/AX-$472rڹ?5z ^ὉٳXD"KXS5"ߟ[`(MPp0Q;#:}܉O_~~>uF3/oopvv~`?q3E-v;Sϣٝlr:ĸq8{,NNNH$:u[[[Ο?Ohh(gϞ%442\\\&99 @VӥK:w~_E+r'aݻ j\vlF~~>'L wؾu+5:ާ;wls2z틹yCQ}@^x%6""B'-YrO!SP٬ I@R1 &z( 3]r=<Ǫн^ލZyzyK;Z88:O?|H$ 7161ѻ0hP~W@gAx~Y*ʐJ^oM#VVܮ ph[ 1;chs޵U?Fחݺ=R:Xn yTU)ILcaa{}寰qc47nx4.^L",, ^f„L4ssRSXKrBjٯOpcS;<fr;9Ӷu[$bYR K1PcdbDbH Ze%ʚJj밷OOOIlnEЎڳ,>\>t桗E }[[[|}}:u*\zGvv6͛>>בH$8 4Z W._Lo$߷ww;vdۖ-ūo,W( )a> ؒi֬_f]^/߃#G3ek88쳓8e22xiL4wVxSTsŃOj$#;;c#NNn3fL@U:**UBJV)8~$Ύl޺ʪj}f0c=j?~XLFf^~mjjHHJaY)jkkͪuhLJ%ȈC HBV{"N* m\ʔ٬t+SrCYEf L b̘1aaaVgϞD"}\b*Dcbb!/uuuX[[hh۶-d8j33svI̞D^X,ΎA ]8:9!Y|0nxA`jQ)\Ԣb"-dZlHllR~b{y/K^h10bjjLqMMxqXZ+ owdY -3UeBuZ?11(g7D"UU*j]:Z$qR) 4-oRjZ^8q{.GRӯH(abbLUUu4 4c+M@>1+x{IIIf[VKW1Bʔ๸:=r\ZZJJr2nI,}Eq m "*(bAc{1ĘS|S$oc=vc;bǮ(M)Rv?VW@[D}];u99ϼ7l Pdget"\iʗ'Stt,zq%xЌE!+II)TYJhLrr ٹ$'Up+sr/YUh*D͔{10=jGxː_愹/"}AO :wnNӦ-tBt$+wo3Ν旤 0 j81z05oVA|-8VFZj4:j_zUgx6$ [L~832XۦFAppHyoߎJJԩFDD4~~RkjjDFF6YYЫW[<=2`d6 O2>L/W&A^Æ_#\y&89S!\OERӮ5ì~^$ srs%]@ΝL&w嗏e9rKsg طo1wcÆ}L:V|9|8XI:!Ipf萁5CGGG |-D IA^r'7ߛ[7w-7or(AAAxN5- 7[.2L   dA)ק_>"$  /2 2p`?j׮T*"  /2 oZһwO<==H$@AAA&A^:E!5jՑH$DFDBWŵ(7@D-Q ׈ o!v9Ax݅ݻ( CAA^#ѓIAx+ӕݺ   KDIAJ^=PM   DIAKݰ!  odA* Lة666@AA "L OGGܹ*W"  o d1F&`$ tdeBY:: 3;`a!)Ʃ"Q)f5M  ²:7PQ[V0]wꡫ'V67(us9ue8;ؐ1ҶmER̈eeF:Yu쀣(4AA(dBo~ИYU!)r/ܽv^EfԠF쩥xT8݄Ԙ#. S3SQ taz洫KK7 Nlr₹tݫIV#HD dBovCWu5`z5*W]><_RJI/E_EݕyXZWCzrH+ޑ۰,~5kgG>h"s7pI>>ةȴ}{py&Lɋ=$-,,}jUh6Ê[^y)w @Ol]p5ֳGC$룹$O[rJ2rQ|h*~#UHLLb\xձ]oʎa`)E{U"o|Gn?VINk qq]?\‚ˉOH,u[eM͌O͚u#6.+W2z0ZhK֌Le 3.e&4ia9\76j\^*VTsf𑣨MFF֬fK0|_~_~fƍ^-_i'%%1)dgw2W;wXh!zznΝ=+gNgoݵҒv;ƛ\NXX4~^sԺCfۖʹm'ggΞ9-7a"\xW.W ʨ=e0yĝceԞR!%D"!59KGMApY;6ַrGhԲr+g< 99 DRuk뒛ü9sѡS.<~_gj /ӻX] ʂgA0?_|7a/E-ײU+LxȤ2nDFjnFF"H_6._DrbcO 77*UU d<@zEI0g`*M}} 3: lʀadB A% RMK*rmDAxq{8Z^NO|\jvFE^~>?<Áз'c #!1kBݳ+؈cG\=c'NMǴϾ&2IUG2`{T*\ dI ؗYs2_oyJ._kkSUSá凟g˩{w0ԩ]/Ukkѭ&Ƽ7Tw Kg܋|A:ǡZUbbPTԩ#9q 4kLJb_ՎQ׹\RdjvX2nNmOkry>kJ%+[W_of:g3ݕ/>YYgcǓЯ{ 1)SGu6=|1#Mf|> ꑞٶsm 3X47N9LVV6u}9u,-֦2E-O7M ܜhiּ9^m&~Mri2 Q~"ڨOͱr-5Q77'73hoX[iݻנcǍɓ\ Jn  ̄}ܸIݻ{x0~dܾ|ӿri>1>ukO2\ξ{O֫O-ߺkۯ? |}e1AO_24fefuf[m/ 2cGjLW\J*tޝV SZZ*kVdp#2sgZ]1IIId: >))U]]} SSڵǾZ5 8x`?۰!^jNN 2 LKKeϮߋC*R^}6o΢ӣw*W hz4jg"W7 DRVYrNZj4`z2tMFVas*U!2+ 69y P<$)<Ǒ}sYt4;aee͒E  êibuMI\RR{v̔[QQ[XТ?իz2-ٵc;֯gtҕiM!++~Y,zN;@Vmj=O^wWXhԸ]dkpEٹ}ϝ/g;:Uû+A>شeKF\tRYvP#Ux'ٙf-ZzG"-`zRxX;nuLqfKHHF͚ΝACRXXH\l,ǏTL&cԘӯ;vC4*^2Q2ۡ^=Ƀ df瓕Ozfyd䑖GV濴<25Ҳ'#+L%R3a`zuygGoCtuپu =zvejy{[יSذ~=Nt҅+/jrm;;+s璟G6m8|`e,Zӿ?.߷W.Ijj )))ڡ&ѷ7{I3RIP*>g6Y둣Giېjض@4m\{].Jbbcb)ıGU=o=2-i߱Uw|z'O`fnƇSߺ +6ORR)))4,! ߟ׮iؿ;v0/061yG''ƎqJ 1U6}g6lWڌ7a#Fjcƾ#86RuXR3`  Z`G#(NJ56WU9?3`Ms\\<37H[4A*}|2/&,c%i;4\<~ 7pߚ6nSžgV-Q(,X56a߁Ø<{׹Uʒƍ|9x8YY3i\tt j%k+ΞJ;܌\]pwJu k؀c'NP D&ѣqW?ߟ~#<mH$de3q(/ǕIIMAF%ӼEKd2+Wˋ0:>>HRUC__ejRiw섩)\[B^^ݻHci[fС yZ tV,GP0xذRc``@nN2u4,,KNNF&aP?|-ca|(: hףĄDJR$U!mؐӧNUxY 666rUjjӴYsd2^hߞB\ůqc 89;z\\6T]]iպ m!G"əDFF!+yJ%K4r=UPh -heudBT6V]24JqDJes9n *˰2ZM󷃭ZN +KBeنMtzaۿ?m۵{^ III˩'H$ 5.ݺ3``llkۗ/_&==.ݺӥ[w:w)mA|'joSSR<y[joϝ۷ !!wO.]DfF[Sڴ!99+/kۨaĉH$N<nsmٸ%nJ)u Hpuw'<{Vd2AvV֯=mz5U߀ԔTpsw/2[DILܼ呞..r<(}==BQnn* ݇"Fd$>u59 MLH͛lz,̳+37 1!<6mڽG^ ~ZАPjU""±RH$|6FdS.܈6#qqqN744b¤IX[[L&C!*U;c[)9o/e($ kduQa]g7\VNR.T Fne*#8rwapxCM7(=9ꖨ>CGFޕ8 cSg?E&#C/aNlQ*ܾ0F&O.wOӠUTdfd!}vF5Tʪ?~װO Fhkn̬-bi=_6=ѲnΘa]:r:o-͌iD޼ŤSPPFFF$&%+kQ{y?y'pT*VS1Ehz*ER÷_bhd]&&p Zw-ktlm4k:uS]>A |\nèfoVw|>tz IflbbJF.*U]RcZ׬So@Hćy4^H$V֚ˣ1M6c=ԫ_ě'=ꦿkvrssUV`aaAAAYYMNE3o,<V^VX:y8[ŋdgeTM<7`Kfff`\ToiǤعV]]]4O/j5޵ydsȡCԫ_07.;!55u%5W-nCÆ΂zgE܄>z\y?}$X]ժLb=ΞUϙi{??zYeZÓjܹ}~dffj2mWEV֤,%%]==meT'N֭(-X %b>r萶6gHR ;M|Yf}9VYCvrm2IMmZo.{y싹KF_On]: 8|M0Xזy{>,+Zny܈-_~I]  Lm<##c#RKOjV6:4 طwNx|'Yll11Ew呞Ʊ]ϝ=K|||JSM̶-C;ի|L$̙,^0_g)9`mcOzܽܿk"CB^eǶn͙S':|[koD&ǍH_P;LQ>io+70@#??_F3OSvm 0pOMMA&}͙-ii?QXXHVV&z^>'%%h®[M2rߚz,_I=WJ+s6R=wѴyR|gfeѺm:t۰"kkkLLL>s ][vk{cdlLԫ_KT[zWȈ,m٬LknӖ<6#Gyy۾SPP ػg7cƾ_lޒ~KFaxRSR042M[T*ŕZ߹zE^hF>!Wp0uGggl\{޾u êUr UruHQ{$I'QP 'xdB.YLۂ <20~+\vR?Jw{P%d#_|q/>aQ4o\qYas)K.[6 }qa'MxÇ `qw2mǾ_} ~<7Wgvޯm{kz<GCTr7&Cި:ӤqC Б ?vұ]RRnL,7i֟1#0㳩a_Վ>=(ІGѡ}U]#4,'ruݻOȵPFL:q/c#~0ظa=j!Ju(J8t۶r|cJ@ ILLzHvzY YyUj2'%)ۡRJW3gرm+V,sڵdge1vxT*W(uؾ̌ J\O4d(3gP*$$$p|؈>&&t֍gu&ܼqukVо=9\|:O䝒H$xשCӯڛPyyydgey\e=&%d8:9qqj5a9u8:x0v*RSSK=W]سs'55s111E"JJj6YATsp(‚DݼʗM^QY> d-yR܉\h.-;T@% 9;Q)ΘKy81)F:\ʣcGSnKc,quУP׿zkC@|6DVb2vΚU+WzP(رm+{vdi9(J߿OlߪysÓw126ʊ5j Z޵c 9}Yʇ'pvgNbۖ,?JEfP*n:::T\wcVm@MJ+g0x0cϮ.sAN;FAAPUXl+8u99dgN<Ȑ#J\A {rrrM{)3#[QQEXZYታ _k!W9ܿi(Jrsr|Xz\0R)pqs#RSS}HxXqr<(Rƍpu-8''=]=::(J.]HaJ{ .,(O*o`' ==/i{ \~PF*A*Q ȐHH$rjH">O̫x8!Cá#A,Z7ڴjk=_N\.g,c*Oyko޺PxWĖa{Qlޥ#o?kj=A] \.ǎd1L?wWk{qs]>6===>>IL2XrRZӿ"F# ҕm!/ZFٕ4 ?M4֥哝Cԭ\ /< j~""ngSiӪY[rGi!CQ*! g ئmyyy̟;M7ж]g)MO!ֺfo^W;yR*H0i2Uٵs'N.05N8w5nLf͸rDu||,^ `+l4\]8y2{wb7XY[1vܸRre] !7'ϝtſuk,ZưdhJ?wjՊ=[3Oior~3[+jaE 9ӭGao| }jLs>\<-y̥[K>N ;OJ}#{"vVrj>FtjN"J3=K=`a*M iȀ 122*hݛpoFzӘ1n$T*wQ&tҳwog6={[+TszYL4#89BRYϜ kqgfr?7^-G 2. \1W 25gCUA&r3V.i⭏nSY3 G;w8| NԭWm[wn>rn.ThA?w>̊@dHVr|o;]ӛzsJܹOJLe^L? "??yʢ}@ 酧grHTd q\NM+TFAx˗.aW;`tӬ:RYq˴ȭMӬzLmΙc;Q%$d)(PfxK,+k+;+f\- $}:r >R"rH2e6\Ϫ!HY|K/Ȉ`(SAxj\ Է-DO&m M࡫:ʙ8529R \FNΫN.NT#駾$rM׾"122$;;繓SV.$';=}w oAn݋$kẄ DmfiaVirRwvzݰy TTYY,]WwwwVWd*oR 4fV-s5SżK6ӫXrCAѰ9z洫KK7 4yqb9ƾ`jՖw  ^8$H033&?lQ2G'V6:w.'_ߪLBѿOWv낣( A73bYkѥiV5;U=AU*wR% ٽ=zuΝA>LKK3vQ\ʔ)?g8pShO.-={8*FCcL3FI_lgK¿BAx'q*1멷HT`kY D%AA(r'2_k33Kp 9ޮ gg/B;?Q<Ɔxy9k q7Ƒ Htw^ۋk&I =LHHH/t&ԩMiܸ 혃->++s-ٽ{[7ԘwNmۈm3?Y>g6l2_v횏kuV-ꋳAw!}ٶeS>PAAAx.2gR)͚ؐNEAAm3'11ŬY6mii|<]g޾23fϥmHMOGjѧO[&NOtf`aaO_g cGaֿ7}lllD  =\.&>|"YXjNJJWkq{hEn-|&Ɋ͗AZqi8]l_:õk7?={.Ծu bc :رʹhQ'.T拳Aw.N@@[,,,D  R_t'fM;S),|v2-[T4ի ##իa"XZi`bb@BT SSę [OGGcұSGE  R=wIWWH;";:Vzu;.] i;iii/P'gOEINNzǯ555F* }X^^>!nݐu9LeblȘC h(AAA^ 2d2ML&e޼HNNյ:&G"ۏ2e7naɒHJJA"uX'72aB FRΠL8篓#AxxTP(thה>p?)`Ax%\\kBxD  sT*I2gΧܽ[[kj5+Wn'<<9k ێ,fZC\\"٘0sL+R~#LL x1[u?~zCΈR^KףE!TbahC#CQ fCA^ e]unNs-E6PEDD4vqfu9t:tOZZ8*b?9\t5"a!hCC\AAz!T/-ѣGk\&A*<{[3͚' DAA]ғ'yz|zzTl8` O BcXP_ߠdKR<ו#HɤoUȢfV#}χAjJe>R cb!$gΞPhz c@ԭWL& D%ʨ=\K.u>O GcڎJǂIRRGRRrgϒFVͥRJP( '4k\.=A7x$.CcfVE7`} ܋y^MjBwXUFRFWGOm'drN\" 捽qw•||l/@Wy <ڵk A,89BPYϜvU|i`醝Plv"g̵h>^ݫIV#J!.&RSSQ(jN8A~~>;wQFceeΝ;ȠYf꒛KFFFFFZF") qd_ »ZoCWve 0="EB AY$?VJ")>B^~>7b y@jZ:JH$Y$?w]vȠңk'֮Xȉggӧ2plmm@ݽXXy J% %))zĂyXr+۟Q7ws8iy6w,q޹~c042 AA3MXf+˖,k/m'|6,Wp,Zȟkp`>B`ʤ:o͛ٳ{{zυYLXX(666'aרQ͌XUu푸:`0c0ԁU ?8'\+&ڞD"A" Efff顧'ra]4jccc*W`UnxZ),4~RīV[c8?\. J)^8u/ok"Rŝ$ q"s8M`v߯W^eggfgg3S07?: Ν=G˖Mٲi#{ŋ{x׬\CiڬyEXj%v& ڡ[е{R߷/>ApH0nnݏ=ʟk\pʜ;wOt܅9?Tnr.S&N@Zs.N;(կɓ zKB6x^ߡGgN8ߗzJN;FzZAB}k4KnߺE_A{`iiTm<~{y8gw׮n(u^=ٹ}gN"//??$ J%[6of0bo/=DD1xTJ`*Bw]$(,,ة3:̙ӧ0hпmwrvkx33#**F ~b1یF#[7osnrlĤ)Sj\ ˴zNn Ӥ~L_f_y )9ܜ{K^^yyy\.OOOlllN`2 O4,%?mBk&(8N;Ʒ_4Bӝ;INNnDG3BnnN i\po?sjj*"QW.Fa@d2Soiة3W˯yjyTyzټq#O`رɖMgWWj=<=f 1ٮC64Ie܌)Mj4Td˗.~ VcjU}{̀A8{ a;w1dPQd/1*m+l`4"Ià7R)$#3 UK5 (sPrjUthJj 1W(Czgs c՚ }y Z-|7L? ]dЀ>;[[&Oc,|q) hݪ ɼųhբ)6l6]NTt | @Hp Q14F ̬  4/]ɖmpgo^՗wPpn;%܅ped|՟֖~'9yN>KP?}{wގ,V!1)Z#[7ӢH$._ gڵ] F$ 017ؼq#%5%on{pf ԨYӦcnnNff&K-"6:wa4lܘ &{+/#>. K5o#X|9~ǁX{T^eױAjF(kWKLjaҒmP34#;i޲ΜF*ѤiR^Sn-7ʵXVp.eWTlٴa#G:VQ>ykWݷQ>8sBj䃏?旟rix7(9+a:&fffFtd$ 6u۶oͪ ~y4@P0fxk*e4GtlٴϾ0ϰ#8v 2]r///Ǫ˟Ȕe˘܌sO2e>,c\8TRSS»]uJŮ;=]vN}{Ɔ&MRvrD##KLQ%99لEdR) 6U6,?^4`^lZ 0(sutt"8mXn]>`h4b/Q\ˎBӕ[\ˎlF#7 `4D&amm^^^XZZP(E\.^R %%%έ[7w~i׎l6o܈\n5l(u҂K.Dӱr2Ξ9C͚5Q* HMˬ瑟%~:FO]9"S(={fh-͙;>jT}Ԋes#:z~ũ'زi#8:: ,})O?*UIoYr|KxF˖ƙӧb̸xW۷ؼq#Ar-VXNbB^^zu ~FvΡϧF;hm1Mx}{̞7~XZZҲu^zRǏѨiѲ%@sj5-Z.oZH, 4kΠˬG3>JEF9v5Z䍊dШմjӖm[63vki׎ ϳ~KOŅ~мnX@mضe3}gK/vN+e{Q9|JoU聥7n͍W. KnqlgF'G*ۛFX>3yyؾD,Щf]|7d┩|߷n{z=ގqMxd)77B"Цm[Bj|"xbB{v3ŅӠaC*#_j4ЩsHg(ҭ{2m'LqqTV $]h tϝܙӨTj]ݷIDEF1h` w^;mՠQCߟ,Z-QL>LFM9y8yy<2>DD!]l<$ FQXZZ`eR88bggiǹboQt8lєaptt`GPn->| ܉s ׫|Zl豓d1|l LzU& ` 0[K(yP\}H`p_DGp/#sǿ];w ##a{Ю5Vr12]{ąݳ+[6dffav0cXYY`0Y ,]11f_qvqy=s˗0ta  ؿ\ߠ߀ ###C8Ȥջt*L*:޶}{GJJJc>@ZmKk:pgP~RSq̄I=v,gϜ!33Ԭ͘A`ꥭ-_~MF@zz:Ο~G_{RKjשiӅϧOe ERI\l ێ?3~{0e /^L.L-5o0 8v|&NJ8| w&//z ={vݺ޵7,҈sݺĜ~BQT2\tԉ(~FP0bhlmm: IDATuݻGRb" BAN8zPvssՕWJ-fۏ%_zQa̸DGFWAǟ;t9v F/ BJJ WO<o6;vb2Hʴڶkϵ%Ȩ@k&?m۩Z&MfС;zTx7cn`ejyIfxLWx=K/akHKMU*6[GnݙٳcrFDEE%FER#v[ N/|T*mTb'$tj&hdæmD\/L53%7ͭ;w7th׆c'Nmnn6M4dɂx4''0 wi߶%RT(cog%J\ݍkk"bRM4tXZZ-,DVNdWX'+:ޭGO.U4jܤ[q>xy+\8/E.3|Ԩ 󱲲BRN~Ri222dQ_?ajQ]>r{ci4 W2jSIooo<<< ޽t QѼ[d2BkF"q qtt$ 0ˍUBtt4 5ƍhK:vBnnL&C"HGi޲%117/w]F{.=z|٦AaGDjŠ{<e[[[h4QPP(dEݨW{vBVߟ`0.]&ҁ1c0x᜖[ӨIczn>femM}>\T*ё7yGG'077`0[{&ұ~\\\H'77Gk&J~^>{b7/]"?/RaCtzW˜>u DLdfdP5(ttt 99t܅v;>1!e{:q^#U VX!}Mb;rCO |%{vqM233ձhסffffЏt:*{Eaa!V:pȈQQ3 KKV.[V=oX'$$$Q00t: vFaO.ƌ5gN|liEzZ:A-Ũ ]Z^ѨܸU||hҴ)EѠ( >,xDG|:pkkL5j LFR5nnn$Szun޸WV0͹3w֖DID?C"-Ҹi}=<07)kTFr3 53~T囌Ĝ\ADx_DPdJk yJŌUkؑHR^+yƼg%76ܻ&]tR_׸v?}ѹ!88 Iz%4Liٮ;޽{JUP<3ddy#ԮSu[..8E.FtR`ʸ R\,5ժW[+ aZMRb"%^}D״ԴRժW'>.<cO?RX?E.vv5CS)kpXtU8{LP+CY0` )>ϭZf06jR^}vn߆Z&vm:tT" 0wɓ 1G>)%%g}²\`Rpqڴkǥp J}-,֖_y剷J~yP=<5{&{ZTbn./a h$77];wAQ&aFDEFѼEBWvv6{FUPЦVJzz:j7UVV 5\~7U=w.E5&1hPtNQh4b0?wb0{[bmmMPP* N'\І=LOT*Rh U[8VZ5AjZ$_2RTYaUVݼxǎ\@an^v̸Wf( <"ᆲy)rٲIZZ*899 CE!Ccj6m biP;FiQN tST)^_ s܌.^"OE}EQ@l mG㥗_&l.n _^}&NLffZ?QSPC {E"+ 6x\.gߞ&o٨qcX7,--iҬ9 6,%qG>jjL: ֱdB>d#=d2Y2Ұwl{> ug\\б>ҕe/喈VTܾMj]-,,SdՍĄr9vv%/DS rsI[nbai\^aLMиIS#iҬ9’AB,--yE&@"h+je"OH:ukؽ(Xaff&,o<{BPA=嫣^ƍΌ/pmjd=5B!Ah 6 4Bk6m܀ra7ٴOVC* 8Fױw0Ir-*W\{]vh8v(zB o%LWWRM=>.333ӓOyG"\.G.yy>Ѐ,[6%xE4mޜ=JhieرHRΟ;GJJJ)8yyԭWZ͛]"9UU3%00{Nق4$1!߿jժ7W}wșS'9Uw( M͘R&ka^QL'My;XZYjԩS++K^:LYȤoINN6KOO i-hْZ HX8`rV:Q$ӧ_Dqq\A=/OR$2YXXd{_H }_^{=zRD h|I_ط b^0SfbgGrRp/ yF JfϛN:V}`UܿAӫO_~[+ǎ)*UxD;u-*q4lHWTܻweNN+KL| !!z| bnĐ/;tYYY%bFt:..|4cyy:y[k'&M.9::[N{gzb UPQpmlppt,KQv_#33'''aI3AIIìH8W:v"..}{vs9Ο;61>>5\V2=άmlJj ;j=m,ޡqR8A{TұsvA|\֭#((DU+z]GX߬EK$  O>f+kkz} / UAAKR /akkZ^ONv]'dyhuXFh407gѯ+싹yY9J4a }PTdddb4d'ӏeꤱih;Uc!;qm;vӷwwԫC& 9sBS&eWxs$z=xT泏w]{f ADDК=|[=| bffF`p0W/_Nݲ6nL&c˦M߷s |ܙ3lߺEYPUh4RSS9|Ȑ V%55~~T^D 8;;9y8kVdڤI:uRƽ{%YXhT*ekh4po QpH0GFדBC2:T0gQ2tʴY &c0Xm TZfa#F?8{ 4̟Oz+%)ä2ѻo_Ξ[tܺy5+WХ[7T\|z YDBz8nʽ{{ PXXHRI#?3) L@'h4yS'cP\DՒͪήc~ jUAvvH$"#>oժe:99y=۷n K TBWbAz-:(JOH$2S2|fϞ͇~̜9#Fxbd2NNNH$ #{3U^777;}{Yh!{vaeU7޽{Z_.@𒝝R,YdW0@~:۷na,e>2z=R(,(P*zEx7nݼ[8wl9U _|)-dyXXZ2di/ZȎm[9| u`aa8s4~7N%-5&Mb4}oܾ˗rW] />W͘d%xW!55r]Я͛cU' ۹eK###Z Bɯ CxxxO&!lvn&OLLcܹdܻ'vv4i+Ws۷nؾMIDTd$ 9LpHpމDZ ;}~+8v N'!zdR.]oBՒί j匟06A\ѮEB͍,sv_bQQ&O1+KtnEZb=OtTIIz=Ç++887nEppIcJ9K M|)r'Nҹ8tQ3$NڱkǦ_΢%1 mڴlα`xHLzs߁#x{{үw8?ܿSSә0n[6a4n9ښio;7o˕SFaoSbi dLNvA@`0gAdffraāAr,,,\܃C)_Ƿj5/2dK{Gq& 1F#gúulXsgϔNPXXsظ~v}eq i#*桷( ifU'q ֫O-hٺ5"rԩrϩW>fff,ZE ~ `N];>e2_ \\]0iRLгwwȯ 0:u1ͮ^8_Rk4SB2olƍ%wY/rܿ$\̕˗)}&ѽGҘ;z!hsY|Z,s @qBBj_fN]mf-oЀ;LJe!刏ߟ#1租8{ ~"3$5LJ[tNQ4c4baaŋy7y;w "IR9s&={dXXX`kkBL&㍷J'Y6i{a4x0M6رTlP5dP,--ٹm;uҒ˗/q ֭cT?}TNpttdڵ4lԈ r5_N;_,=yR3>??.y=q[ZX}6Mj5l^-||}iݶ- "&:m[ҠaCF~M[*۷nP[ȴ7ʊ+Ww6kF˜xuf^-j3` TڱMeI; e֭\[®VNR犾T*ISa0صc'Z,2|8i),]7Z{mJ%{wY"772NNNtٓ-6Kn~6xPZZnߺUn~;v$6?Ϟ5ЩSeů۳WoS'MdW_bei[[}YϏ]ݣgORSSYVZIvpqqAPЭGO6_ϟk֔A6n޽8t`?3O 1ڴkǟWh/89;ѰQ#ٻ;wlgY$%%ѳO 󲰴+KR"OڵA"a9O4jHRbQ~/ LV'{133cXo IuArr2!:2}>ɹQҋRGZ_Y{k[ziӦt:|bffر*[ʦg> /像?׿+֮ew.>cj }ק3 %&?43>Zw?*u,!>@ ٺy3v'3 6])w;oӧ_Rcvlg޽|?kv&^$Hi>V}x/bL&g!eU4|Ϋ *-0ex_ sE&76׉' ߛ"JOH?}*֚FҥKj-\.jjlXM6`ffFVu"b\t *US@Bq-;1[[TuZ`*dܼy jT"IP;:J'8"H bر:](*ܹ}S*DʲxBmlSrѹkgdfUśWGַe8q$%H,-0bF*L/NL?N:amc-f""""/(E-Մ!x[%s.#=gIUgTj\AyYǍ7¬YeJ-Z`ĉj TJB"dx"Ǟb<{)pw]<t<Sq$EƮV=iڴ[bR:ʒ9x(de#IϰsˍqcGѶmEqIDDDEs7IJa\ˎe=̴׽RLeQB`Æ cߟiӦѱ)kvv6rBhE%ѓI_Tֶtr.`o~c`Ř2 V U.E$d*QQ+Qʤؚ< r/عU}<=z8ڵ6"""""/g38;krpUSӡ:]k=v L kkkܹݻw8KVVVR r9r^G*L"""""=$/R /Y(+lrktZz 2::* ; T*WfQn)N#MFЯV*(Jr9 (VOQ\yˉpqu"8('Td5֖VXY[`nnFZiq) Z+$222ueZ#Q#Ѭy3Q\yl R nݺEƍ1 撟VER  E h4bff9fffDFF""-"""""!L""rrrq+77\0h42fZR3tjji 8:9aooD"##UԩaChޢ9bey"@NNNԨUĤ$*>>>VKV(rHtƆ*U (2h IHLNߥvhMV $nRBK A3G=U}8p%La4iXFVDDDD!חc^GP`nnL&C"p]hޢf B+Z^DDDDDyB$/DT.@*oW/H1 ?<^4Z4kW^AB j;5/"""""}h?11jŒyA e3 NjB>b(utB ??oΝVS ʕ̙Z޽'ͧN 끋#$'O+x1mBW㟱aѢOUܻ͏?.رiӇѽ{Kӳ7Ξsf8;;TS'o 3xѴo߄lVΎG""ЫGGDppX""""""""""""ϐG LҲe=ti!|pO'`K@_}5nZUGÆ5ٵx>}_ |83˖me̥tҒ|=&F^ݛ{hn ~3ɴͫ_2c 'O^fK\_7pJ4~:Kxpuug)ΏpjksAYj13f|, L""""""""""""d231nRp r[[k,-T2v#++WWܹiӡ\x L[}{/?HE&DBa^1GoTJA={̴zpF,|3|3 NZ)KKV( ԨGBB 0SP.nnN\v1gM`+C""1> 4___0DDDDDDDDDDDD̞f+2>};o֬N b{O2zFa?$)9͛q0<=Fr0gP;ZN-",-ʤH$ TGL}sS-?.omfƟq,]6ͤIecooí[ jV͋ <&N̜9iذ&Mo~C3L>Z#%? ƈdii-2GFg$8$T\NG>}O>f!ݧT~S,Y\¾nX5ٸ*hV~]L;jP_U焱c byITtҿt ~-[n?۸.ԵǏh/|U:v,ua.Y,ۏv:ôIJw{~DZp:svN8NVVNNNhՊ^}":s79==fSN\tӧcck+@dԮSwdggc')))XY[Ѯ}CBXv-7omW*G$A`ՈIJrʕ~>Y}LWx4=ٳ\r2]7zZ*>>oq={1d w)"[7o&9) ssT\8];=k3!XXZOs6Wy*ǩ'8 8::Rn= 5+M) 1B8gܸ}h+X4jv<~;ӭG L6 ;;Ǿ'~JEu??Ivv6TڶmѢUWdz^oo  MﶓꟚGwU{{'jzzU4;kY]] MBBzB = y|N9wΜ93ywFV؈FAR5zIRɂLcs,}u1FRQEbBD>;|[m?$PQY#bldL7oo~]4 nn,XS3fE:3ߟ-F$ofיƮTUV1h&O$I ]v 4J7r1OΚ$IzJ035{8|]m[9o555tܙf=MGf׮VBP*mtuuqus_/#...][uSZRBxr,]\3VXXHXp0EEEXY[1jX9Ea%.]DiI ;0jjmƞVcėf6c wjkjXv 1HHG;IP* ڱW7Y?D*+:|89 kkO|  IO?;c8w6F_>R*+INN>חrJD@^|ok5kxŗn{ntTYYY|1̫k79gӆTnͤhJrb"=0h%%%%[mM !ܜFӡcGٷwЫ]yz .$'&6qv[VVJhpP(wL2U>DaM` U[UYIYi}C$=<9q8qu*0TWs&6YK#/͢m L Mg/_EdqC6}4031ӷ͘b,fݽ%fn2غ K+K8:~k9NtTEEEڅ9XZY1h:u|_e Hӫ+iiXYYqeyα䥗 Ջyy댛0kkT*&O]Mqq1FFq#vvv|7+sOX(cO{vQYQ rsr022d7s'¾m[-y>x}{0zSw161k"+3}j5;V*-?qgϰ;4_x#cc"NDVӻo_JKK BAzu_CCx"$ѷ_?v󦬬5++''S6_B͍$&:B§m?123Kn49N??O8B$WoyLKLPx8uu3|HtttȾp=èCǎ3VHl&2)صX uu"NRSS5{2mY<&+oCHKxtM?cO-C$j5 Q__O]]M:::S0s/1ꋋ8Uѧ?}7%9)fߏ@/ ٹ}/R]U|CC}=C}rFF̘9}VXNmmA?T*9D:Eh.=ܙ:c[SS3='L?۶ltT#Gf!$8Eqgh_&ʻ:::3V0jȡC ENڱ- Dh.AJXH0j 3yT,u223y-OwMA3lݼwf<5KoZԕ˗oDFDЯ_LQQ\++kLNiI 3V&xҥM5^lL,ƌeK/VnQƑ7V߯mGbt˞aۻ_BCջ7,]J8~H4d_@]]e3zhllJ]\eQ}|8y8~(IS"?fЫdii!rII\c3>ZnvbnaKF>}&j5ǎEK^`)8żVm;**ʩ͒Ŵssa,,Y|f6'O'2]AAA qȈE?/Wb־YÇ/<(<vw)OSߓN;[MWYQAٳ׏ V Uh8t`|i0w=.ж/NN9t辍&m׎T{gzZzM{z.N?!!?lmm̎m[;{C`ooOjj*55 6L@'IgϜv1,,-xeR ξ=[ ]\\[=e!5{fWPov͕;eA,X-3<3g.?1kk.~^^*j ŅgOr.-9B`yط.4.0ݪ"Nd_Yj۷n"#"XWVdsppy&9'Xz5׮%,$Xw Gs1V)..fڵ_54{4]õ[XnkWCۙ֊q /2~DكJw3u }n!.q:*ښ6oĨcxW033gWP ӃīzfԩTUVdBN?ι6&1 IDAT!C`Ύ#Gj]Nxxzq1/13rh`wHt }}=LLM̌W_fWW_rmǎ̬ٳh4 #قEB_O~!*m h4th]ppp^HvvqiŧuǏё'LAVcdh2QXPSGnmecci剉)ڵ#5%28:ʢ';^wW7?wg̖Fヌ.554<=g.W}qxx]5 ?t kVʟx-v{;Q[SCaa!;t`Z+fZojd^~5J%^]!I*sq<6 tuuŅĄٍz\INNߟd\ܚ=C GWORICC$QW_ GO~b]P񞊊رm+=z43Q^^FVf HBvC]X"vgun)SyëK_ؘ<ΞGhLl [ӦEf㲾 IOH u\vqu%-5"prq;tޞ8Hb|<8:9!IOϙٶensP^P__c .XCYY{xͅXX>PPpI|&6CC HJLW544PW[{[n..лOfߖVZj*ĝ353ɉt\\,"#"xHlA̧AGG7n$3#'gggegfT*ٳ;f֡ii$3u  .}3y`ŝŧ/ݼ}x"mڴ#KL}qrv&??[6h p\њHmM &&dOhݨc[#@w__ :vЀz kk3vjkIIImvA;ՋV7Q\\\9Q$d̙c"FԠT*;;;/\s.0ɖ,j5'NwL 1cOE)D&!LS˨ZT*T*4Y+e},,-C7ndqr/㲳 7'5%}}}:\YD63ࠝ_Hbb"=gp 1ښ233ڱQQ<]+"m-ٓ VCCCziq؈M +s_ [dff?qO9BԩSZaw[Q(#)tŷ.8t uuu̞7' TW7'ehd$',4BݨMټi:::\Z{mΟKGylrQoŋ`eq|VE[zMoh4c=TWc`h)~ WWew˶rp>v܁ݼt'skMTQQg9REs3U̼E$I}RC8XZnn+74޶e UU|w46x[MۇC+g[([. I^]ɓ ˓fvmxk?77W@(,d޽k&2{\رm+:|x6P|UeeVN2Οϒ}M^+geQRR%ڿ[[afn&/f܆p;Pv+^EE9T]ފvl>|o!zzzMwڵkGdm8m &,4 '<< y&MrE,ukQ(LWA޽Y~_yD2tp6_OEEy3'ޫ[W~GN,VBYY|j䃦\iٱm+)XXZ2uu444ȋ'Ȑ')?g%(7s۶˗/STX@L;wlcHO' ҝt!99 x뱉<}nasSK?Ǭsċ!|V(**es3)**Ɔ WO-)LuUUR>Ldٸ|3~.TVTp18:TtvLeE,愅~OΜOw_<e~ܕO|^^]QccT*uuuxuWn,[nP]U% QU8 # tW,EH}eZ;q-xvnLhks꼬N;}\GW7wb}y=%Ib3ϐOfz:kV2ytlmm133gdg̱Gp'sccycȥ%%.Vb{-$&&uֆI ?żg``hȹ_qSwrqaۖ~O'$$P,b2  ? +++ppp+t҅~Q^^F yBx;tt!33W_}Fa+ɚU_̻ɉ/Nh.wf0pc;"#rkjvlgۖ-|dA[eİjȧw_q{JIJ8y?|:{е7GguJ{$%$0tf'رm+?~=j={&%9`KKgM` tΎGoCC022B$%';񏿽7R_Wvw]æwҶWT֯]ˊePRRrL,u?s`>o݂~=5z .bEW0k?_wް١h8{&V^յw#f9ˢsyYtioI 3#[jڹ'BDGV>ŧtq@{,urR ##CZyz.yqJ~~>.3'NнGs~S33&OFAA-mF33*ʵ8)A;7f DZ#Ghll*u=9/9II444PXX/?HcC#Fjf|VG.ݺyN]\8u}V8;}Z+TT\qGsԔ#GQ^VNԩSum<%/x pjkjΖ=:tHFFy$^Z1wVԡCRSSwLԩSr6{#Iu\~~>&&RZZB3ӧ_?]]LrsԹ3#FBTRUUImM T*y^XPPp_uZݝsqg)--}wS^^IIR8zp1Q͝pr}t#6:J-FzȈ<ڭ iA!,?\5{.^P΄Jㆤb``@ii)]tsHf;0>ܙ3iz?HkF=y2$6&~~<3g]_x +|e2O??ϝobODRy& ֌5)ӧڠiؿxzu%!8:9ݗؘzaSOrrw1z229s jΎm8Çicogi1pylZLfxӞ7mDFDp( 6,ZB]:`dlLjJr͙7uk~]%Ozܹ eoб# _rÅijJ*J↟SRR0IaB{扩xGCgY#g)+V߂v7]4SكBR0nD "]iI I ׿%%IJH`;\$M ҂k5mpyM0wRRTTDfF>t_~a@SݑFsȑ۾ضe ?|-eeetԙi3$##C/4u%~Gtt{zյmOnjKHp0_&L<R!Cٻ;RIZuh&{Gdmoby899+7]z hhlkD^S,ڷo{{ZtХXN'ݱܩ$ CTTTؤ'dK?}0˒$™ؘlߓ=G''}q}{f͞<>YZZ+GHOgS6otX{C!Æӧ10gᘛ|oN6 {{{6mXONN&߸a2oniwʵrںgO=ƎǮ ~tpwh=Z~$IbĨQXXXRVV6idVm۶ك}y&LLӯ?'Oҹ#;uՍ֬A$mq.賾>{;3g|ڷǧ/]R҂W<<<ٹc; 2@Lt4Ξʊ Ҧ=#Gfq Td$j i\MM Y$CCkԭ$/\]033'.]o?Bxz֮Zkoqbun4?8x0֭WWQ*0]ATVTb߶-&tHCC.uϝvFJr2?{INJGgg2dWɿxf۴iügRȾpc>ʺF@i榷NV qVzTK[9vwϬSW466"I={sqO>$$$퍍 uW,]x=ҳrZ{'ï3;pL??vlFXhVb>J7ߠ#?M6}O>+bG4?D'ٹ1^K(/T[[]g`PӧdKXXj&EF^`֞̌ MN97_K^{ F#ǭk9NCcCS^L4Qw;Oը @:T*$QQQABB;vhhhHQh4-N ןӧ>Pqv($`?alb´3y `M`? 'r!bcby=T*gbcqh`펗EgK1u;]|?nRm ^z~J|Gh4ڴiÒ^zLߏ=Q+h%\HVa$x~X,<6KL*5]| SOM<":U`)++E{:F ط&>xď:##Rh1u;{O8:<Ԕ9н؈Jj5~_?dc0@ XOyx0zG IpLuqTjxu{=Y1x;J"VڿT* ض#i3%ܦ@m%]Ӕ[]ȩ$vErV&'À Z7u.ꂚ7ܓS|A4;@VHcc,0ŕ.ҫO_@&"PKnˢ3s%,@ x1?|5s71YH7% ʹAbrIVݳҵHD]]ͬՕc-4Y0566K@ xp2uCg,ND9J3)+ooEvK@ !Bdd_!f( j5 -嚐$ Bzzz!I@`.$@ #Bd_j-)wSpRRtuu144DT I @ 7DL#t177z_+25d$6ꢣY@ @*BdφMۨoh.;iM.r$ѡ@ @*t9#_t? ==]>3aNt@ @ %@==]-x1c`ee%D @ !2 8f,xFh@ aR3E##\ݺ 6m`ooOmm- TS͘$IBhC=[2%&$Iw[7&L{,Bb>|+C@@Ott@ JzE.ėfCb9t)/q!I/^仯\AOOZMhh(m۶%** I3f ь7J%W[]Mue%oM ڵ7Q %bMH`Qv|q9j{O.ΚBxIIv7L߾ ?~[{Ñ#ф[y7v.6n `̘SW&GbwEOw2s |{ qI Ddq[ @KF ;FVr8yėfFu[5ݻSXX$IjHNNFPлwo OOOJ% F7;na䘱8:9)r$.]NS8uLtttةCENw 3&444p͒_:y81ёcQZZ[4h0}DɓG0ysdd!!Go+o>>DDh]]%$1o{50fӉ @hŊTTTQQQo0bDoJIty&Dmm= YZmݺJ Wahhٿ$_`Ӧ=L0H<Ղ~=9IQ(A@ fT`4{Cgo.+kk:uD~~>:t]]]LMMիڵ}|H;#@CC444Э[7?[UUI8zʤɓX*ܜ&<8OΜI}}I | ?{fy3ºRf&MnnN55~dgW_aii#()LTd$糲BWW}{&0+++z{ȿ>䎬5zɶ-[8ysə˗پu {|ǭֿy&|tԉWE ++R"SFF:VVV\|Y; 1gh3zqb1s\|2{<e֭\x:fU *w>tRf~VfoQ,,,$,8"5f,kdfd`ieŠCԹsk3c 4{Zht<֬o¶-[Xżx}{0zfyvSw161fڵG6ܼi]u5˗9zsBh7, Z'n𐡲PhpssggPJKJfǰpx8=prv>s*$ ^C#!;2zXҮȤTbƞ `^n.V6]3.G:ũbmc̈́'/7$L@Vf&g%={rP8$v.{g)&ӆ al۶R[;.M`aae8)K2e)ӧɇrZ ڶ];J)..mM+ y=Vh4V2lm϶b&kƎŠeѵkW!0 @9QxQ8oS2nZLLLg 8wwwT*eee׏v1x` ;FBP`aa?#GdԨQ o]AAaccO@bb"OLC|irڈ'137o~bM` o@Xh{`ر}j=;%<1e =,Ll BAw__gemfBw~=$Ąx9ObB<1,X6o^_?y~s̞7O. WWwf5sssw̜sg 2zann""mNEFb߶zTv&wOI?wVSMk-o7,]?[$Cvk899sСe)8{:n)m۵)=-Y;sqg[[mm[6cVΞa𐡼ۓJmM C &кI3gy>~t)C gߞ-$'&* L2h?{fWPo6ff} R|=|5Z+%%% Z~f񪕿_}jX7nd=X 'O`ڵ֏v`ڵ]Je~}gС̙7q&wwؕFNv6sgμ6MhH0z敥KiߡǏi5]nN_x&oT*EEEݽgs )tQQ԰y&F /9nx $%%^mˤD= eܿ)gkޞȈ8::q>+K~SRqski1x/ӶmfiHHǿgχ}{_ЦDwz 8r4Oc$&fsSݍ >`޼I&cjj̓O嫯֐|x!011&11Ν[׿dٲv.JJʙ5}n>}ZÇy8~(Z]nɉE/AVCAAG‚y.@VrÇ1lp Xz5|9w?L$&&&\ˣ5+[~]ΒƈѣO?HOzclbko[ٿo/ôn;u^knVVVɖ@CAAxO]ysq/^D{w$IÃĄe֕޴}(** 'w耽=qqZ/>ss $l۲Ev!sP{om[clbB~ ɡػʼi=wJB@*" vWu *VA"H/B )$BH$3r$$`,>ו0s<<=^}?];v0d0T*) ?o^'ښ*4 XY[JvV^ޤ>̈́{yЂh4rp~N&5Y{774dpNYvum#+tgµ\hƁ4ϔI`4g-ldgWg ;;gyfo xqۙ39|rغ;ȧ6"Ø`B?\"2\k4Ӱ0LJ[Vc4CS[[Kmm2^`0`ff/CTj5ϿgΤg|Cpn/б#ǏM۲:jYTI=}Z]8^ ۶1\n.V˄oݝ?oWĭ7aر};Ι j6кZõZVV˥k8N~9F!dmMM!%m\\9NξQK=JŁIJL3VVʹOKKNÕYf56?e*67U]UERB i^wtXbRRɔ)4x?豅 ײpK_OkܠA]J-;ݎ!Cd2 rh|j. !lFhTfk~7F%2i!RՂqY;3;Z/P~DꊅeJt)--fʲ9g qvv6旂'ggh4r&=WV,[2Ҙc ?ꊝ=6lD6($4 +kkN 11~o7$'3믈ރ;ǏLJm۲m*++Ϗdfdˋ5?T(566\,m`b{bْ!v]|j(/+c޽xh:bֲa:ʔ0gؾ{'MKp%;GeE%XXXm>` :33K|YZ ggg||}پu+a<9s&S)((`7_EdfdгW/T*{!??% .f-뫴y=()Ъ2% s(/W c׎[X0b\~ #G&i+GD4;?'OWJFkx8ǎ\Cǎ諫Y?իVa0eSPռ|P~i`+Iٻg71?Q~1L$FoUK>CujPfn޶-66F>[&iqԵB!h^7;s8]')hqʊSOaoo`d2QSS^2 Jk@^g2033C`aahdŊ"%T*?4 cX[Y18h0U44~&{/*Cs.L2/||}5ǎݣgx \Xݻ$ 0K!C,^Ӧ7 \@GnUpkߞ=+eYXX9#Ņ{'Ofk}ͷXlǏN`f#5fغjnaѨV lZm[̝wOhx+ov.8Isvfps|Mֹ=ǏW_}hپ}{&O吡pttbO1۳33sOzZ?'av]_.f-Gœaouw˗qYl 3=򵰴d9)]ZGϞĬ/~V:FwWhI9}=wSW[qC{曯D1bhڴi÷a4)**b󦍘տkl 9 IDAT `4 ix ֢{ȈQi}kXXZb0HOKt:z쩼-eC}VGw~v]pJѣyV1c[;}gK-2FcXS e帷m1מ'.uWx ԉ|:cƶ-[HNJ"0(??SSZW;we\4777x@vVFު#LUuCo ٣Ffƃ9jnnnr BLlq_#s?۪ug 6V KzZ*joKKKZ26Suu5uuut:t:fffhZt:R!,:->x\%QkUUVC3wLПz~b~\CɅFCUe%&xa0N>?w܁7>K=qti++*C0T)dHyVqaf!3C+d2amcrC ۵cu7JVNLǃLdQH!7h B8Uܔ5ע榬!^ F#xxxPWW7_~I@`{vgggPTؠj/*++ϧ^}p*? 06ӭ$0(|-QT0utаNYf5+- s<;tkgQQ[7oaST[.`8sm7tJd2T|\o5ҒI܊o&Lff:}Fqߜ !⏕VG0#W)kؒ{#fkۮՕ+m޴:t:j5:@z2ZM,7bGₗMqz}Y~Okx8?:'3 Ǝ8FBk/>'08aGүUB&q B&;[kC nԴB!94tppdG4QA8M9*NbSn,WBW%!ޖi&3tBaie9,Hk.Tn$BUI$nA7.drv瑇2h mBq+-Nba< %g(\bnO7=]¤BJ2[o<۹3mkP!B!U2ٖS'ӿ?@B!BI 8ЇO=077B!B!ZIB&ǔ&󶞘I!B!IB&ԣOww7V+o!B!ⷒڵK8+wLPT*LB!B! "5l0tT$&AHX*FFB!n)H!!_H@Mu<2[ڈc!\2H!B*B! &!%M<1ԇK&i$B!Bn$dIac  h2b4$\B!B!2[3qj$"B!BQ$d}.F&$KB!B!ğAB&?Y1f. !B!%!wB!o[xJP=!t #9H I!hsJt7Y/Mbcqswr&I`4(;4B!_MZY 7p$˅:x1g8ЄBIܴ,,5<[֑I![ĿfPhhg@Ne!ؔK~^ |]L&*C\X@]]*Jyd2htT_>TFj5jVG=BLckoY>j$m\]ZaNB&!B4',`G$3]s)kؒ{#fESC@ªVByy9&*QՔQRR|<|JLLSNXYY1bm7'+L8\Z܍49ؿ,d߳aO|lڴisuJKٰ~=g)ϣG;6b$:u1knrxzQ?9 cnn~TVT݊8vr 1R *Yl)I ; Sgz=O{^~?LofwI %;ǵ3}4(ܜV,[Fjj uuuk׎c,s6;˖qL:_&Nۥ/O?ƇwŸ;[lQQLJK/b2Cc!=rիV;*exv 71sVyYsfLxx8Flܦ8z?$vvy_TUUð#)))a7_7Y_ֻMu>dL&qSK^ΐpptB! }C&@YfKao.3Z`bMx{{qFJJJ Z T*UUUh41 Jdnn=DFFc6ْс; k_K3%&˹s?`{/y^<ã:iDEGc08sZ8'*:W7wN?Kљ1hǫPXXH.] tı摙ѳW?|^;u.~ErY֯^SN.J͡X#^-:tHƙ3QՄwh#7y_J ֶ˗^՗^w߾߱$..|¤m5Yc,?^x 6oڈ NF!5%QRDZJ j;IШ5?3d0{EeZ,^DHXMw>$dmylփ>xR B!b8Uc3q|J2-Nju9GRSSVEJ\xѨL ,,,8~8J.z*t:h|Req߿rS&GXZ͍Ǟxmc9/E]B&=CSW^|o,&[Vucbdfd`aiI}гWo K˦!F;Yt[99AQ;|6Ό;ܜ$%&1~5۷Ɓpff&]P"2*];wR֛Pکsr75BI<;b{LB!/<[n: h J+8<ǎٳTUUM~~>yyy瓛KUU666O%??/oqa4!++G''}U[8~ ZMp232Xj%ĎۉFRpJYgU9|!C1hN?uꃐŋ=x^zcgg˩eyyyKFIHhYQVZJfFFm[xY.6m@qq|ٳ|IF͛1o5UNft҅ܜ.\d"1!P033,]CI=$5%N6A!p*h$ǘ:}:*a-.7yXXIFK^$44???eoшdj4.SC T[[pZd2accCfVVɹ\Ő멮¢><F;FAA{vi3h42ᇔuzCdtŝT*{)S0LQ^Vhw>֣?ѳWoRSy)..?7oi'*2ΜʪŁ_@ +3TV-2% Gn#11.]jē||Ǐg};0e4EFRXXC(oU;9)yF!aaNJRu GV١ԠeEۿVNQT\^ޔ^$[7mlxYlۺM뻟vؑǞxZ87g}fB{z6"h4rfŅ(P~B9z:w];js?e*>KX8::2ញ|՗MZ29882iط:qJEyE9x}[6)bq:Nҥk~``5?-&//rVb۷nVVo^ORB:\}5}u5O'Ã@}˷OfF7;>v`aT*R9A~޶kkI+kk\]]ۛӧp}MЂh4rp~N&5Zf޽tF#!k Ԗ?H}B!P@E5iFKyo)))ARQ[[d`0(] uuuʤ$ _45SVVV*B߮]宮?u*@*_j(/,,$4򜗗ֲd"rsrh%%%孼//oirsӴkV[.]R8::ՇiЧo_~i=舯'Or6;R""y1LweK+\. Ԑr:ܜ+WS[[KMM 6!cggOYYWWFJ. \̯Cʩkx8ZTUWRnf!RWWGvVa󦍬XGf>t1nFT*m{гCt g]Z ԉ5dj9_|.D狋qwwsƙ"QQ5~*D޾鰶̌:rΞo-k_߿9K۶mAi]x3<3݇a#F^ӓ_yIE/ ׷jfnQ,_;w nnn=r}@搚Bt:cƎ}|hwp̌r聅%zBb~\KKeQ#0(];w0tΟ?OB|<s2geYuA[ZNQQ8~Ǐs@}8}u5bR{[en)?y7F_P$%&5*'7WJLHLz:F Z(mp=y嗱cAXXXy#hM={",^l`kIOMUY2kl% ;ˀA z}:䧘NCh߾=k:zy)r8KEyL17tffnNEE E5f XYYR5p5AA\Xu& b׎ܥ+nݽgwn+00wj4FUUUcan ^ǔN8qwOmF{OO[0~.^,a]HQa}6׬[ðIOOĉFNηcxy{hp>SL V|I. @={/y嵿?^oν&]X0LxvSZNeMyGgʕ=r3xP%`oͲߒx<ڵϏmlv6.m[Y[7   tǘ3xP\\Zw]eK7$\WSDnF})@*gaa /W,T)>L<Æz[Xǎ)tьp ]"#2u ';+/El6|]V,[;oߞ?h|+M>_xcGѪDd>ST*u%%FW_|eo͜UOIO'$,L39ڷKp.YFс_orr66- eҥTUURVZVʠATTG IDATTPVZ?vd}ݐ{JbQm+bbppt`Ԙ-^ݝ%zFa1)r۶eܖ%N] :utr2''g""m ϏTݛv޹s{򘛛f<` ;+a#FoUN*BV}nfҴID聮͹oo LF#ф҇_~~68!/ 8/‘ݟmպ3~@~^ Lt+5RTTtFZ25t233Sh4899Ȩвq %J9f U֛xzvW^]?}{NNb= [22P_ЪeElټgRߢ02jr>Z۳kZ&7aԷ(T%-*V.)!B|uTIsS0ӿeAܔ5W_ U@nؿnqSo2a2D(ҩjԙ^vAknٌJ? ӧTT< ߻}{eFjjjqptGϞ׽cGrQ|nHrSeEG!Yeq>j3L諫9ǘqwүUZ26Lt]"o%B!&,`GMsSְ%0G׶_z=qmW<2c:LAAhFzZ&>>RB!B!h Y1fKB!B!h5 ػ}O) !B!B\ ,,5< 97ww)!B!B&2XXjx) 9W77)!B!B2Eڛ1sdB!-t)! GB[^ΐpptB!ʆB񻐐ն-z "B!BߕLώ< B!B!2"| k)!B!B$dҩ-3^}`em-"B!B?LºcڣSݯ/VVR B!7ɄJR*>,Q5`kkJBRa0(++@eeڵ#4,B!n&2COLgޘK!B\$ƓPrBE\ qKA׽à[AYi)'`333!//ZTWWS[[ŋht[&LJI!MKa2HIz `Cս;f.]hd4b00MF~ e_!&,8UqB3_v׵s?VZFbffhdÆ mۖu֡R>|89h4r9zrBܔ$d=I&գfffR BB&!B\Klq_#C=r 9*NbSn,x)lr[5qORXXJh4RWWGRReeetڕvڱpBj5'O&##'ObccCHHZd͍ .0d||}d !HwTa?""j4 !BVie9J4#,ʅvV.ܔ5l=̿~Vh:Z PըT*rssٺu+GaԨQ 4JEll,111qcnn|fee5:Lys2r&L#ӧs/x53#˖1h"u l$pptcG/F܌S'ow`QLkz599L]2GfO1={{{B;ub=^Lz: ]]K7$\ڙ>C^멩0h:}G̏kx5۰'vlNqq1vvvt  (FŬN>}:l8* ^$($_yڿoU 4QWW5k8rEEExxxлo_ t)}o/s]kS޻ӓӦK[eˈwGyYsfLxx8Flܦ8z?$vvy_TUUð#)))a7_7Y_ֻLFKxW4. !B0 7\3`R2[r }ov}99;ӱcG N-ݻwÃoRxWҥ 999TWW@mm-ҩS'ݯ FsZ圊#"">37ww7ϟPA1utEF6Z'Y$=-:>ёsyƒ{~Nc,^'''wm[o| ډu,ԗC.˲ym*~޶KOTIOg֭ӳllˋzr4˖,\n.NѪJ]]{ؘL&073gOFfF. ;+Ӧ7Yo~qy wߏF!5%Ѹx&* Gb);IШ5?3d0{/jkYx!a7&1}Dh@B!n$NdfآLq<Ʃ bZmBCCIII2L&AAAֲ~zd27`41brT*666oߞ T*kC||ֻOb~@zŻolo1k4,Zlzx0iJǏӮ}{&nwX`Vtbw{?tM/FHhi899qye??4ݻs.7ww߾Ծ=ǎk++_WWWJmn޸FKJLd玟³CGG |J.\]{O]obJ2XMNNNdefRSSN#==Q9[$'&2ϰg֭,?^|S'TSQQ @%*:ǟzZ!c?ӷ_{zrHEy9 lv66nB m5fr Œ׿@Ͽ/<,o/Cc >c#0ygk~XE4jʊgϿO<ȶ[xŗj  ٽkZ;Ǐotdtjv]ڱn?g:xFCtʱ%&b1hZ-YYl޴ʊ <;t`JF!F;Yt 7''6//>tCPUUsgFGnY?agmMLt :bT* oԹKWe{m,qo/""%`B!/0n 4l!22Aѷo_0 \x^zA8p AAA :Z 2CgAA͙JfА!ݽXUŲIuUzMB"fT XR~%>8wODd$/_ #8q8|ơEcai5kprrf1ٜ<qptO6 5%4#Y|IN-2VZɏ >^'P]U*wSTK{OOizѧo&'[;;#"!`OᙏVjpƙ3||9?ʕe&RSRYj% ktl;7_~X`a7_s.7 .b2/]ʷh͛6ʕ:TJ17e;wwfB(,(hZr]@}+ǙO<ɨcغy3"lLdÏPϑÇbw1tpxzvvsc &))QRR"A|PZzmc=ygpsw'|||`owt2Mv";G۶m-SZzSDFEݔo%ӟ)㞻 ~B!3Vݰ΢M6ZBwXTgwPT:bKl؍&11零Ivnf1fcb{{TH00cdt,ܯR8uΜ933%R)ڊ gggTUUa2R)2 JB@"X[t:J%Rӭ´0L2QQ,7 TUV1a0X8ogN\``DwϏS'NRSSJŞ=b}d'ǎٻ;_| }J}yWYhYxyy>=WP2gP\\LEE9A!!9||%_6QTǎwix-?'ؼi#3flڸA؜UUH$d2?RyNKKE-cɢ8)@k=/n<^סƁJW\A[Vpq&<`FᤦT( =)\x@mgG3dfXjrrM[-cۗѝMOII!(8zn ?ڝ?  ִeA2 hmYrZ`h!l.svvvd2JKJnݨ ,`ӆYZ͊_q!/SXP@\|d2Wd8gKKC-g^ٰn-G"0(bz 0vnNLl,1l۲&t"3fzC "__49}F ݝw[ QXP?l͝T*%S'3v,wY8NNTVT.f[fOEEy0,-5L&w'֢V#Үi.,}Z-9.xMDHkHL|׿l~#;oŅ@:F׹3G/m[BeEnnnZQRTds6mXǻp<ҐJaZzꅽ=|W1 def|F}l6X|M͖pusyy*kBzK79lZrju{r=oZש];vLON:cǚN1>(/+~YƲŋoӆNC&DtNt=0xw';qz,8?N :t0vΆupqua؈M{f* q~nٕ( 0Y|џ[\|AlݲɄL&ÃMS]U5l8/V\FCk@aa![6Z^&MuI܇!Cش~=)IqnKUe]{.2@d(tʒEj@.s)h P©j_{^܌ezBBCnX*||}:֣GpstS(ΥW_Nl8vlFzZaav+Zx{{3Fr.]"aȐVk'{.zb4[?Ќl2a41̘of2]@1$@4AA 8RƧI V2-ZvQP[ƻ&ӥ@j֭[agg\.f`0P(P((Jr9 2Vŋ*Dݺ5Y/0}CaAN`Qwǎտ/L=z?k1h~Uylۺkz}xzzj^xu"ILP@T[%=>z^`  <`Ҏdf_̐ίHv- L&~~~a0ʾ={pttRD"\nNWSSCAAZGz"%9g{Z6ӯ𑣈h=QQ^AChXwuBv6v@T\_:y{0e6Axf$'eĨՏUd Jf>aC+Z2 Д\>1 ~o4:my&$ȱU?ؒc F4 TB)멬D.1pPm)4>6r$&\\\ 9#,"⎋?,Dt¥Ç)DL  JC9o+nӔ[Sђ4RAJL-ڑ_"HL  Nfe.?em"Y{EcZ`A*25[g{e*菳d"dAq(m6Ez˰*gڻݳh$ -$ ߂[g^~9{W7  o]p$ =$B&ٗG^h~#H    k"dNNT{{{q@AAAAIL@Xf<={RAAAAhtԽw(SgLs׮"\AAAA ɐףL>nP*,AAAAKwHΘJl|grSPAAu9- oHhX?wu@Ʈyy gwtTRs>); f<⊦U'{0n";F!Jų_s};ޞ Ͽ͝JZ)C&,   fdRS}Qs{$P߬ h0?CXDGʵe\˵\[v!pըPs)   f߽P4vfS#כ^Cؙ4z$$ػwQm,84` ?_O?͜@SՒđ{mٿ$ػc+vP)D$   fd]R#ǣ`m"7˥e2c0?9z2j wΞ<#0LR]T*d2Q__c Cnu ֭#KZo@\]]+]AAAfLW5F*Es`'5_~a뉌e三|2Nh2`^S'g?ͭd0Xl)yytޝ-O^/M pSGJ8XD6"}9*gڻݳ]6KʑH$~i͌f<ܝ% <42US} u˲&͠ARVVβe_sػ|S>s:8۴%S4/Ó{GwJKظj9^ެe1{wle)l߰ww|mI _/tBi݉t˹Ĕ^&}$s'g*e|Cx`6'"*&_S"+vop|[9ס/ *fL01a$ħY|0AVfe.?em"Y{Ѵ"}9 N.8 Vw= HЖWj^r9FDe `3d2 O`F"AAp %{F=|49m̘A,Ys,gGy\5|w,oWr)Gy Jփ-kVZqr潿G'jdTUV0dXT*Rzd2:W23mOX<;Zə;!H NFFF@UUe1.t5ǿ]`oC,7$Gul߰:}ߓ[\O3ϧ0nr  :RƧI V2ȯ ᴲ %il;BoŻ&u&RR񢶶ɄDR Ʉ 2dB&VEG:b6NAx;ЪҲiOII1/#+=y@t kby#Jٽu3gJe%E^J|Z#Z\7kG.o ka&ݲ|[<3Ri󻻇^7̯'۽1`rqqaт"`A2+s '=ߋYW-&-ؙwԢl@*VQL&RTB.BJ{;;r9 2)QpvqAy/\ȇ Ae Ez-*zyudr&gqin631ǟr{y,] Zj𑣈s9'L*#0(& Eشa=#nݣO?7DP4dge_BP z_sqVXuG0o\y=۴ᡶtb:߷@Ue%_%111$ 1e&N?K9tN@HhZFӗ={>|!EqfRcpv),,Z0Ieud4RHTWaqEg%\Ok8q iIgX]K@BiIu:] EU{7ޜKor~!|C"vu55憇'he|]0( ৬M n0϶c㐩ӧr ɩb^R)'+;KSYYZlOOOz?!l8鵬Cz>7Dw!;+ جF>TWWH^H%R9_>6mr!;OR8&''9]Ӣ8vxHN:&L̗^``ߞ|irO?;ӯDZj*ǏeҔ)}0 ,?!CKoG&q͵l6ypc`:GϝC&s9zM[f˗k67j)ޭZv(bIܺpʵeTWGV8\=jqusC"pqJ0 G|\RΜ""*;r>5MW5N?b3}B"ĤOp|.^ĸ yb8/]ʚU1j(- Ց4V6+`>h:SIGJۜ\.G*bRR+).)cS4KУ{뛼g`|֕@.ogϜ柟}FIKMd21tp*+*Iܵ{;;yyRSXb/]v%?/w~G0bHΜ|AɴHuU=a49{4s/~tl޴.W޴/?ғ!ҵq;, kk5X-8o 2qsszsO>߀L4̜xRzԧ)6+WOmOFhpu)OX8ooNΓ˖~\.gMי^}k67w.OMJ=6b$z,\ovټƛm۶KD"ayh4=۳sґdf% ^ϖMC*HϞDv`wJpHH6PU]ZfP`||})/2x8?/_f̓O= 8CՖ5jsVkt9'-7-ΰ#`Ob"qqzDzB?` rKغe35iC!L2^>\x֎n;z˽LZjO lv/^$SgNxND"شpur̢9z>~|џڭ2ΟCTG&<=A$'5ԍ/2g8r5kP\\LYY]wo|G%%9 _3[4ޞ)--EW9oɢE\Φ3O1L}z:z!9zڿw/^|'}Vx֮^ETtG"={f Βѣ1HI}Ϟ=Ct9yXh˖,jOJgN3gw̙t2..p~6/n-^ 0Ƚl)?c4/ؿw/֭?6:ǎ51 N?F.]u| t֍|طm7g{‚;FNe0_{''g֯[w}hAZZT#}PQQή;xbx^{ }|8rA\pz^;NXX/-}8\mcrRR@6%S$vf{$4Ś.|`3foʯ^|i84ԪM[^y ړHiLyY~W}P<ڛ6ujlMopxo|qӵ>Gݺߓg̴g3>jzGMQUYP$+pprB[ZBPl<M6-Ǝc^q IZHb]ǝ0A*s9u:Jʴ,]oNJχ01~jy2b nSSNrYeeLJgHMNم ` 0uJE}}=G&4,IS`6I:{ʊf&xyd2޴Ёsii SL R6mSS_W캖Ez-K $$paZ,s;wR\\l b:qpfʕؾ---۴mˋ/B}sGAmht\t߿QE6مݝ:k-ݷЦm[N?NTz=oəӧ <<"<==qtt"%۴ XjdfdDPpMFy Xo  $3#n().>`l2Q\\Jer 9| 'Us9;MfFaa\G"X[mؾ)ӞFR]>rTWϳ=oݳq'H~^U?'N$/7];v ֣A\r+K/#ɨ"0(_??:5,Zӫ9r/_ =kuagOQaAAzx:dp;ҵ+|zq=ᐐP$&2pPt4v@qW^^VC&QS]F9.. s;ab72L>xgN3n$yOnݐdm;Vj,ݽ iI~e5^;A:+Syl!ܴrm qӞ!pkqpr]P0Z-{oB:LBN..\ 8''?Ţ9N(ƌˊe{k. Ezv(r-ѰL:dP_Z-'33'Gۃ1f}yb0=СT*HNɾ ](dWY>_W'. ֭J~>wD"a@Bs/s'v@ԧR1ʹmдkpLYKW4 )/k4x\n$yn^388Z$ m9>ĝ;I:sd|||hIDH 9.q`>nŋx~_kUPPh$77D˝ oӆN1lXZ"x_JwOʚUVVJz}-{-=ys|Kڝ"+399}1ֱ҅'ߦ O"syҔ>yP*ջ7~~TWUaX׭qpFCV;{{t55TJ:6oڈT*6N1$=KhXM+‚*Xt5H~ %LJ.ϐP.YLxx!aa6frss-|||EWS=2z;99RXpf}VVV|߯HܹoO/O׿{'xz]egeQp%}uuMLj;;<+qEcg6٢z6lW4mO]+3_{>CsݓpKONC!͍6!)IDATSHD{\~ӓghMy{. ,f3zɀ _ /L$eЧ#TWW-[o#wgúlڸJb̌P;z’ B"pE:.]x(  UU֛ 6{yudeΞ[N///ٲi3rZKAk4=sT}tQ9tڍO S'zIII kVt¯@'''>LnlߓHDDzd)Y1)0{6Ny؝jR\]]y_?Xd1 vvkVsGGxuS]Ue\"0iS۷1ѷ]gQQ˗-z.y3'oLhX8;msii<39ͷ\- t;\7rO23Yv ݩk#pM::f1 J^}=̘+dH`4[ ٻ{7j;;"[j;qݲ yڲZSrFɺf30  ;]]p%i-2w$ͺ;%J":K/<6m݉;wČ oO/$l6a2$Suguh _~Y͝˚U0 ੩>c?ΙC⮝)ӧNv; d|՗(U* g˞eAVHSB/ ,!S(ǏkUA.ط''gk Z;/Kcd Jl >.^̩'1 DuFTnjnqU7YCΥK̞5 \FxMvo݉7z˗p|$H >+M3;.I ]6GD"w>ZH]]zjqphfZnMQQMJVX33cyl޸}תUD"aaڱ °#ezBBCL#ذ~UU2t[KmgKݍ-:DEq.=o77wb;DZc6 '08 ||nw%?\k8ooo?;HΥK$ @Jc͵:=[amxŧ޳g}U 4*8  ܵ#%i|o+٢egea2]ZX8Nve͊ ӧ3FɌ\&G*WR٣Ra2g֬/ nc^9~_tѣk< Vj >X!lAگAvVǏ\O3L2'ϞDd2%mSt;#}n'gۯVhBA=HvԖ1f/7j jˈti6t0S>òe+ 퀣 Nnz삝ZTb\4eͮ;P*t͞Ŭ]c-^ .H~Ӻ5ٷgxَѝpvqfq!qzٓ{Z䤳7kqDzd tͦ2 (RVD9AAʴ}b03Gf_mwZڶd"((Ҳ"M:FEP*ǿ55:ʴZ2){z\ lFGj ܥCCNE=QPBKdH"8Xm t55̙a{䎻%̄)t_hul2a4YM&L6?F;& ! ծ B{NSnMGKؒw2;&נ[ IۇT4 :=ᡡTTUr% r\d|?  /A^jq:4񮌝4d28  C{8䧬M$k/0/k 49oK;&ȱ]ol6agoOYI .]əag{шޞ?A\z=0A ΄)%AAAZA8Yq(m6Ez۞*gڻݳ]`j ..HRrfGGg=F*Ύθ{ K1qtrO иm4~J?F.  ]mt+(,*C.FZZ&:F3aATVU[@^^1Z""ɽ|Re)AMCI2 o^ !)  w ᡀ 7W*+kpwwíN'*V vv*$% h2=l>ApX8:  p.\*TJ_ /h2Wmϥ%d_BMu%nn. qA5dz1 9@qTAAh(7І#Po0"Y# T RBFhX$lB!f] 𠒼O̓G uq4ld2a60Lf&&CAAAA{PHlIENDB`icingadb-web-1.4.0/doc/res/icingadb-web.png000066400000000000000000014755101516124654200204310ustar00rootroot00000000000000PNG  IHDR 7eiCCPiccxڕлkSI*-Z R;VHKr')R\ׂmп@WYEΊ.Z )B CԌZ,v?.ëkmc&6aVW ̯O0e9^E8u>nL(|ATL}KeRGwqeGE*Vs#\q)Ze9YEg1㒎PkS즺y =UXUU^{SK s[8=v$yF+oڣ} uëw9[ TZ{&! ?SM1Wc_g) pHYs  yIDATxka* (mYD*eۂcК[sMb,*ښ*&j + Vh/MjL,˲ܖ2ͭ3N3v1i<{9쒓L|>3yN6zIo>;=vZ%S=ވt(X_rتXa۲o ԼXC, vMѧ2vslQlqj}flW)V (S7xYwܶWo%ތ=+eE/(tt]„J:wbNl93?mȒcI 9F*\!=г =ԽЛiY_lPuiqJR]l/ ~[ܓ>Sm ctG)G=KzhUz;*fn+؜i<Ol^OO`cx99%T{k)D蝱=m=;=iaԟ .)}{Ҍ#}DA^ۓFq|P5cs؂/8+J7n&^|Ќ.ΞU3^U3N'o?t(\=][{z'g:+jzӶX/8OdѴo?컦mw_+ P!O?+i ӛV9}|4筍?~Wxgй)گ?qJ9w ޝ'y74={(}}SW63O[@H+E&^\|Ɏ>3S:ZS|(]_\ƞb]OO$5=\ݡҭ72ؓWS*.S&V7Ɔ+|I?-s @[N SON:maGwd~(--=9O1{V78}96Cm;kWoXz\;a•۲7r'^ibujckbu]Cp @3_LthS #on /ےڷ43*5VL'lc[ۦ;в 7 7Å U}\Tڷ"75)@`~J=2 @;+yZJ;tjØm;|(6=oPő6~{bZ 7>{ե6lA{sn_9>GXz[no3U*DŽ'L]ǃ(vwgOwk){tg(m淇vKO׾S=gOG'LYwЀ[o/u9 ~}=ʖn.#P?.f{5piʞ4U- @֧^CyW @)ϭk|`Z_ngwg;Ɵ%C+{tyޒFbP}Ǟ¦g㧬W @zˡn9l98-4;fb o3~_8:v{4~[׿MIg>;CcGv'n ;*go7ԦhC^[OOYm:6magvCP+2{˞;F"˱##PG[bS=H@R-f4|,O/o>а%4d#P>Xk S89CPlL&}n5#|nc9OG+ps^ƟxƎ@LOHW @awk6s'?7 e᧠ o(W?T o@ yak8/ӎ.gɴ:On+gefJ]z@̚0t棯~FCPY|Aknu-~p)@z5 9DS;yuKO:{a~瘿Sm+ntz{'wISРcHnk ̬O7ؓ"\liϑFM|x΋!~Oѧp),fwuσSZà%maX[F]6gIg*IO;uص*++=Hϲ#m@ Ɇr$lAҪStR"8Uk[ZXFZVJ 69I YĘ)&bdo S8px}{"`5sa~d;@Sr#O E=K?f~4gḑ"Pn{&7ow>oG3?}Sb~?XؓR)1[;ySݳ+h7VL s9џ]&[YE/IG\]u9?\؝OizG3RhRZy?mO~mt};:ݠo~m$^U߲bF(?6=>Shv? SV/e"isi܄it.?,Ul~} = E ?́-Gs󔿩Iy b)i ȳ? @nėnq*E o'y<~ d@j֍?=cS*κ%м3P:sz?Y~5~jMtM*( @rx",sw)iS"OhϿjwW?W3}>o>rN*vU37wǬMck @z6"揾=Gٚ S6s[׿=KǦqG^#gfuznܟ]O~^@2 @Snrν Y웦&TP~@_X&Y7H0)k&@Lz6dS1-DVo*(GQftϴ揌Ͼe*$ y|캷cc[6 @oQ͟?EX?[}l;>m @SGvjG&P"Y<䆜|w5aRIoX~+3ҏ@zJ~Qfa)x{G&O~d6o??qYط^7skܸ#R4s-]bG@UϾU ElIn5?QH > }4{3L(p]nWjHZ1Svǘ+4i~@*OG~4B}lɳo h'Q3A__QzjmSGV6&x"IM.y? Ph͟)Tk4LlA^OoLOmY)E<~7+kݘ@>+ecZv:@ꅵPWiL'O}kF[?޻?'O"⏜-3׷Wnħ}+Ȃk"M̃R[@vRoTONmMlL5E~o(%-ɷPmu̵6 ?^R=ќGOLso~(15%7>^mxv@2xtMS)oO4/PnnuM??O<ɋf'.} ?Gso5@O?c&Oo43q͋?~dx-<:@_\,"P|MHj9H{js"?? 0D*cAi(=F?wlz[?^) 8/8hʾSq#>*?6s9:@qr8aOȁwdd%bFPf2ho @ ?RG~+7/s F}蓫o/ +Nll1?"z6z6dfG@O*FHM}{z'ߤN%7 )RMxup2?RGsYUhCx/s5}'Ï9o6sI|S-Y\[ 0 (~TPi 9z|맘 ?<@F}KAξϊ/7tnGm VY/D36c 6'mt܈O\[ݟf}c*Z:<-9EVR1sou[_yͿ94}syG?O4{>7ࣙIEDJ @YOzGݿ!(1;nїkH3@dT_.܋oG @gy:x7r_gdxW;ٷo72> @5eϚgO2.(Y}\(HmhʣO (8fLFf qw+'}M߉ Ep*7djG&⏦oՅ3WngWP~,Eo䎒ASrP~l{HJD Eȁ5Y @%Zqzm}|+ZtkG3~om齟0طf&IS߻x-@E4+讼%Wݿ\xw7 @R'jkOj'mhFG3*EٷǟfyG.b7g7QT ".#(~'"\RkU<&n#n/]P?2?O ۃo2sMvz~G7}?d*ϟic8qobMERZToڜ?bvd[@fbGL,w@lC*Ol۫nѝϹ s7ʶ%,mux@^)LmYLoto($L}M0A={˽SξiJEc!Xڛ$1eco2LG[@53kXkuϸ?tͬmv OON1h[< 8HG.._=b6HGsEMsAO=6}(( Xşڇvup'^kl @? E?bO?Ə}=m>y[?RS'vf9K ~v7@> @ ??un==|S3M @~M qz5f[?ο>?) 3;wg/ٝ 7i A)#o^򜕅G''oR(0K8%]n ܀Sz`G^Mme!~D*V @5~8fF'~^yM|67R>9U-E[^t?kw\Io4.m OPxM34?GsPŬO[O }gp;tܒٷ2shl#'ঙml٨c@AOA^4 Ys?۲m}5w5@?^ p~73u7=-9w͟RP ~GZQNьO-S'>,Od_^E _ls+>~V1>BP4ҭPk =_1n[~4P[)rϨ3o|rCgW$3#0>qwdM@AҔz'ovi_Hѧšr9=KCg @9&}Lo8ېMOy7n! @RHfNNXذݸuⵏSH Od#*E[>Z'A^f>/:z7|(h*{XyH6qm1o;W0[R.HFXXYV{;Bn9Sre5咲`L8"}g9if`+z??!=< 8%+j'.i)Z;CڍAeewiVީQ䏜?ZȍV-Is? ?,~rk׾q=S .Nek=Ϥ',}R3_l@{q!@O͐I ΀J59_tMT3h@jOdG׈^~hj,~OYQ⇧]ɥgi?,~WwXŏ3SO}QeÊ@dOYnSHpd,7 R^VJ(2‡PzBBmѹ4vg*Mk%$AQo/>|Iߊx?{?,oӼ.???Y7̭~ \K E-ⷒc\, H \bUT̴vFi*h_ke?;q\cǙv U$[  \7(=A99ȅ;=9{={qsޜb{}  H3%  \ o7  Hnvo|o?aH@cŏTF{Av (_i34 X:UED]YRKH-*z_\O݊ /|@4y"~LU8pU;ΉSl) 4-|w8m5񪰢AAA /9x W02AA.vu\w˲#GZGiW>~dOzP䲃4.(h3V Bs죹hʎ#T0;g?t9{F䏢4ȟh79o3g#բ%[;1s.uĝ@"D1"%XM7'GAABx|  rNaP4&ot8:'OjԇzczfO dDAe[(=~ZR{]LK$GòG 'G΂9-\G?),Jh $bƩ/7Q;f[+A\r?sv  ?1-dhdOh׏ u׏ҽׯeWN#򇫒?Cacŏf5~   r!j,G2>  IE۳W ?. d:D7"} 7@\XyFfsWW)|/Jw{g_n]/yh̦VBy/<\?\~2lV%|gGAV(GnK*gqo e웫' >XuAAAں79 AN@;۽-m @.2GǾi_.=?"7E\ޟUG zŏ&Gp><[a\c-~,Ku)!;>R? Wf<4\?!  f`HH  n" e^fD\k_Ճ"@Jojע' nFkBxmnAO$2wc~>Sdnb $!m'g`ŏ&jO!ET09_}o5i1@[f%i֮,q{>=Ns~1?_]QYUOS;BϬm+f}"ȞTAHP'+{qwoLŏa@~:zE?@%~s%?IAAi+ $3S  N"TG|@"QHI F QFeRc߸J'~H~n;COyz8 6@Txĝ>RF\[@oOLA#flP 7>sZޜC o5RG豷's#h#U#ՒG =-?sTKgPQ?a| DAAA@|!AA.Dff'#zHj28~?=cdhhC)knpF0 +r^1zrAZU!=Vןd %qln^ǐ4qNQ('Th7%GE @?Z)&HuhЫf?R߫Yh.='\'   ]@{#)BA:Ef,˟\bvP@G|S{t! Gq1&b1$7Hď{cr³DZ~N(*cZ,e.iPK JwoA#*S4zS+rc+07yTT+}瘥L2!ߦ+#G kQߤ'@-P,?n  T-4?q{?Ο~^2tz~+Q"/|l ?)@#AAAQ W͹( A6,|ܒAx\k*Ql͒jp}k'}E^vϑǏt Vp" g*4 N sQ>r@y4Lo}X<~G#g%"FcLM@v8nc'hԫ ԭKMV'Jwhi:FmMЕ b"~\ܺ EaM3]UȰGK 󇑚b2Vď H/J|`G:`ŏü8Rƾyq acl̇\o/'AAAG$\7袉CӵL HۋSh8H{&KU%zЏwܧPCPבץ^%Hr# T`sǕlFd<h:Uq5 *#ެWHɟe GA:l{ 2UƾE->qJ=CcgkWG Vc߾T]Gh{Gy)_ڒĕU[ӘG+PV0"|\zeREo9Zٮp h +J!&+4V (Iy^dOߴ"_N  H7G˙ A6 pa!g!2q\W})}ڇIuׯP)}z{!uHɶa[̚"#+ dEH @ ?gRߌzyAP((P|Q7''kr=u7Gw15"SE]R{q zm?-9NUR ;4x^,T7< nc[%[ dН?e1ErT?젽? -?n?zOHW:nWsf٣%z&AA9o! Ap2CpU%zl'-2x>C'8&K~:[eMz12.2A;dcng:^IJWH PR7AJz !TxSoн)E#5Ru)43@Q%\@n$Mg6Qe!z#f@НCŏ)frN\DnZ "2@Sk)ďTv1pZe>Mc-4)Gؐ7>M eU_P  H7+ A VtPRd%\!V^[[XR,sgBEOsל@z%ّY4ȴm$φ4@[+. 2 9v]*H{<?5gďx=,qawiGG4`JL&T5$}>*kM-zzt@,@@L?Z另CxPET%S)]@@~#g?=   hJ#\Xb_K(g}[=y-}=-bB^KW>9wt|cYiG֤zhtmK 'U䏳H#@oHP(w0"|%{Sui6%f ?^6=A Wƿ ~41H k[kRIoܓ;iځftЗ1]@H ;; ӳ[>u,lD0# 2%~4~T&kO&H{JM"}Uu޹Cez^~1M*cP皋_R  HDƵ? L -m}]ѷUgBL?5qL>dz,LrEhKYśI#⇫ǔ|񺁤+Pn;AF!@SjP4Hw(hKi:rܪ;!vN?>JOY /'xcW*qZ;AwGZu#QbgEa$;SOO_k0#Fm7?n9G}Xf+uԥtNB?J9XX9BΟo  8#e{E~ $/~4#h*~A!  r> a7wcnb A..l%!Eg6؏jܯ![ݗ9kq>M I>uH"գ&n:UXM\J O 5NP@3]"2 ?"d@R ?y@wGNQ?6*[{蚒8]?=A7̌Ӎ z=.'?Y<~Z]!m=LfĽ@ŏ_]VzڐX=%EZ[kwwbn{ߴbw\:HO>HO?R}8ؑoJ @sX#U=? ƾ-:oZhDWK AAAY 72]?tb.a.err$鞾ݕtN?>[f?~g rG[hy-u^G~~~:YM:(BދdzȄꆬ C F @~d? !%ߴ:}~Go'Y) \Kז&hX@Y4+@',nHPb:>#H?%Fֲl&=% $~@f\!Ind5EuZ}|c#6MуWPI.)ѭ,/w;hrBGQ@GUg2cѸ;lzH܇^ңߤZ.?AA90x^/NO7ӗ H J_\bR)gD5L E%9P#@(zLG9d𺴄qF/Vа8I 3tCy;pЭs<'~JY2'~O_NϧPvPʌ[ˬ3<=Ǽ.= S Y7K&gV)f?^D7謻~ƿI-w#ǢvQ(ct(` >@HJH5tu7 N̒)Xy#AAiKHt#NLWWDeL/{Z ulNJA" wȇNJPtT^d"C;aq>>G?yյ5sWp pZ{772vݳY/;:#~ VJ7PWl (X0XdWT$b;LU'~bj>=Oو?=Gi@q- rOѣ @^˭B1?"!) Ϣ֏]Vjwyhؼ8Yd8#-q_@fg AOhi$wt-?|OO2+?L߲ Joo ?R1QGwEw3v<%ߴ2}~I!  Җp_LO'3|Ӈ1.gF0@ qpĉ~z-Y܂Dp>K+Z_w9[R?-X<y~mA٭{@r8yd[ K !D@>h* +A'}D(Q{ Ɏy;y?kkv)8'6,u3|X gW]pO&(h^*ʟ?~o92an> 7@W>cOw *[W?(wϝqGP$J| _a+9)@%!s)~\YX>?v?R|+lHG zM qmmU@Չ)PEQeNM̀MMmW۬)z^>u6m*.666/(J&!vۀ9fdb lorCC1&Oh#`7Wg=(1`Gug~0keC^bY`(,7Q~ e 3!P0`lU`(_(;{&,N9GD) EQEQ d"'}',m,)L 6ܶFmS(8H !b0a .WX @TsLD?w~&ڷ|b\9|/pFsߢmD `la'R'2ٺ+11lzI8b !{mďY1~7q0 61ok)L7fa*3˿ _x ޳eC0tȽ,a 7AG)ȴzWŎJ+v+mNey@9Wn<3=g,>WFo)|=\,=ES^ "kzC0w>1_5-wX3ІnU?Bh =⣧61oa1WBscθ>\mƄe>؍_tfRƶ5s}EK;qcg䠢<,協@TPWt>%!?ϑ =o,e$a~Q<) EQEQS6[ϟvϰ+Hd3`̦kFUEyUӐ*%ǴamYZ,&ۘŹYl,yEd5cGi߱G(~J$~d }&쎋% q>< (Wi$Tt9H\YP\ș76gulp) 4s)Fk+ vH¹˰j )j8:aƘi0n9Q *'<2js놝0*(I'q羪ć7`i%MÆI\pjz`c o2nso DN5 0DEQBP! 1a834 mc%`ިE((ʩ{e t$&5:fI)9fEr^O、(io{mztVdE04M"$B@ H[1#D ]R$VC} yO/^PNH"8Ɛ?I׮(8w\aBPL()?%e?>n܃csa9U$srB#eLbo8>2ow,++?}1;_3UDv w՛{qY}Xܙ=6f`J/G2RYA`E8w\"DDPaJD 3aS$^P؊,#,Tp\,56 ! ճ}HսD4)ʫ?(*t8B8Il:].|^tL9s,mnr\ίn3eG^Q^T \jsm:cσ*0+-/-TeT"r]LqO\9_|%%Do YI͜I/^1Av-?|[r rI^Fԟ|$f4 #* Zl_=춹ΠiU}8&YcDeP1"Ex-Ad8G"{9S3_疍xYS؟X$e, 1p1'5&<*?&#" L!#2!K1oݏ_soįՋw] z,Z9Cxnڴ^:mX?؆4Dd0Ysc`'q\=w1n7:(ہ q )'p-N5%\-at." (Ig✓#R>Hʟu" f~Wip((/>nvW_J3E9i'9''Zm.l*/Y-}=FB.ccqٶ{GO%w5Ez80.B0( (pP&rSh A^PrWMV.щ25ಢ3q 9$R\gё o | +vb2˦ $suOtIw``IR@((/mNǫsЫ?0+]ؼæDm>efwGPC.%5l's)$ؘ%e\!BXX@?Sd;5_9y$J;!'{ #R!]> CM 'ERp\‘ zd oA>%?ȏU3L2d`jˠP9|ۊv[L>uN,Jǣ#Gxxw3[B)!0::+@Rb쯢d Z׃3K0THr l&g²]T%P9>09eNdAh> E((ʋAf W He`3h|f5eww:d6'K2YGHN}EĒ_,LEI싴Ml(M=~ļbw@B댅8a!8?#촹(+X&22# n!=BĪ38jX5zAmMy*}MWu\xKr0a0`Ocnx8ekpn?a+dFƸ*!c%,J@DXJN`l.+NmǯŠJT^b1m)22`q59r[U>-QS)FfW$9 (*EQEQ^e~%vo G6!CD}QNZ'"KJ_aWY|F;/^R(pɲz?Um aӐ$f̏u+&R|5=xCB㈘1j؞ &JJ0 .&3*BRܺg1.Zց.DOɄԔ^\yiU#v9Ebh?RZܶ2vRuD} ?HQT)(I܏: E$,XLy mR6e3l+'HM.J}=XBD+uq9f!`۟k|qb9Hܣ  />A0Q؏RpR qQP|aʀuV !W )Ck8gi~E+|U !sjPn2`ܻ7@/qK H&@!R {|1zn0ct+pYqVQC͂8.nf:n=ɹ W2 ITAP+a07AD4/5 ο#Z9"(?V SnPTߪQ@(('K,i+,{ lf@<3ZW\nӉ6U4|ωh96hU@fEuLϸ",KLT Ŷ-8CQ!%ǗM$ =܃To\ aB\Px$C06fc IC ׎` sa– *bb_BrT x.3j!ز/h9H'`θGcv6_t{{f(= S!_>feX{d]ߍk.XM}%07$ ͥV͡qŚnTکp.X O\1f}U XUv;Q#ĥ: -.JJƸPPH h߼1QbP ZQT)(`M;i 0tIW(걟%%RHo~BkluCmn hMm>fFjֈj{"‡ZTeEe"}Ѕ]۷l7r(|Oue^EE rq!Yy.YyIz{Ih#~=fAL(H,? N{6X7/lKFk +c`yH (;P>w]: 0kwd4tؑ1p '6ἡ$BGXvmyD|#9rm#@%v$o_ 3 pS7%LJJP\8+Y08/ HQT)(ϐ ,esx^xZƗRxSm@k6ߵ mnZmի ΜMIl ѥ$Vm0lI.8K?Rg䋐-y /~?DI5!"KJ(eQ/<k;_" \xX:s ,HH,'*~ݾ7>1/e ! /jj1@<=^eMvqiGPKK櫂JD ,v3J94o>3߃AnQkrp;Vg dt&aYHXhAm9Hp7*o !%iDQ?c22pHQT)(䏔!qR4ie'3aqI)HQNmC6g.\`E6_צcN ݽ~δ[m)ͬr7h/Y%*c>-_?M"9#:pmdI H$+{~jWـY%-;ȽO&54?5ڛ}hq^Kmػ7_ܙ&".EntIadx |>t0ݣ)p$=I*z+i¶l(.9K j6W3~b{S=+X݅hH V# VU M2&율3o]݁׾qIh/,;WVT00;dS\bI$&E7: uYREQ{p[Jxoz7ߗr@d2 ۀ 1ۃ㙱0`0`lPF(Kws:k>__R-}KաRWsu뮽=f 2 a3R5% qdFnDZSRa4LJ[/0giv;weuj1$4 Ä^Q uٵ)1Fqڋ@Og#ְݱ .Ahx@w _`c.z.\@A\@l#9uA"T׼UԌ?JmtDrH6mG,B Ep=P@bK<+'uc;>1j'9P-}EY7l BqjK7K u 0jrU!F|D4wp Wn+B 6騭;֮NB2X^ ?n׍m}8v}!YF9fw)q-dS:ǁzw6sUQ^&㬀i3ni]FB"rMGPE%p[Ob)"QtKg >^xşJu"jXGA35dKRq'9p ?-6q~qVtie2ytyض4m}`Di7^:6$sn7j?aF,qk~t9"Ί?K߶.<1X07n/]Ȁ /(:NnI "S ߺG8'S٩vb" 9rfB<;=y4"l?YZ87a*Q/Hxp@̶14B͍3()\-xC()_Zj#6DCW_\;>Ͻx--PpyxdFRr[|:55lEivmP ضi.[?ݷzءR>&ΥNw8: ˇa`CA7=uIKjf!A/Yu0 N\"/+}\~6W^r=H/<<<<<<<<~aBiƄF&v(VPǰBGdi4Mi9zc aӕce$Lz-l]slZKҌIYkBi#H yM!![ǡ) 5iD\{o>g,~ g[q'֝s~pݱ _cw#dDz:d 5z2w]kZXb>sXUy7N8W/Q*c\'[r(М9wwkK6!k-ebCv>֙ohI{#q[7-="n䳵΍qd $.ܾ9?=m= MEhcfY M^ZS3v.P e?c E1^Dm4bv vp?6s^rrM~>@(s䴗RcQJhIV"d !^A/V)5z,-~*o^x\0jX|ü5bJ 8=k9|#&L8'O3.{-nrFܔȐ"uS M6ٕ8CJ${ld̔n> |y[R"K _ͦs@F+4&ږBݴ]bS 1"P<ϑoU,𓆗6-VxP;NYdXwǝ Qr{qJc^Ɂ-vOU ߼Mk)o"iv{lI鐑"N}!Czm. %"Gq8pmb2=pE32+]6 ŀCvO /#s+X mc Q3#5E=X 5gc"cl=?6S+ӓ4wpRC(0>~T5mi$wϗTo=VP  ) Ec]JmC̹8{8[yDI! (B=u([$9K+Ж y˻<<?eCxi``ٺ\&҄[!\o As%hHt|9lD -]ȱ}u|i\' u}NxhhGqd zxzlc6r\JPq_ Pjn)ͫH8j맠/LBI篃:f(!K#7gw EC=)@vxxx1aOaQHDpDr!u(0Ed('XǜfEq HTDm+-Ɯa%᰹n4bᘤ>hۙ>>MIӿ^Y/ZuIA ABJDT *iG [,qŭc*n8+OcǪy<ex:lHxI~?`C6aݱ7 _aiPKz+O4R7>0RʥObiK]s(2=6d)eCR\r9fh3%j8 <75#( ܱh  ۪Gl09fA,3p|w_t?}|up ݻ=9xu=5=G;' ad7qY:Gic9q >lxDDv>ĺ*p|Gqt)5{1V : 0=1&B`8/j(L?PB{PVY w*hS/Zj@_iCzW{Ŏ$[y P$ۼ?E~#S)!4PV:bJ\-)[uYے+Sl*PT R * ES mk 6:U!B( Ȕ A)wM)Fє0e#&0[ŔȈ4 ֜ ˍkJR d$ॆX49Іnx]D.u5,96\6 &s 7t-afXp)Bp;Im!9J9!G)ȶ)m/ζbJEbgŸhO( 5DB,Bl^7='̰-$"z?ٽW{ o?r >uZ\i^ zs@Dq mІznG3/\߿gMę6 ḑkYkJx3N;;B_'`1(lm۪sO<[g%{544^ۗ"0@-[O͏(>yk/&ɜ:nhw-pToEF$w@=CEm}FςgBcXN%f'2$> <<czJBBEӵ"5L5QDQL&b)#Eah7b;7Lðd$n4j5T݊J(T)X* (("<@yPEwp=J0< {ȸ\8z2eQfRGP c H@-5hҌ'S ~F f <?莊˭# yN&)72t }َ;+seTowb a/qU}l2?͆ډJ$S8G+ļ "O;/ѽpR$O\35сD/x"d p2QGi|7O™ qxE|%mufs|kDkCAC0m"}ɓ oipK܋cvcC +b @䘂N|!F+[uC"g;x/xo 4TzD ӞCPgذGMǟ. =:@p^X=<<<<<<<<'0~hي Z)% Oi9Q ?ԖACQ٘fT@x.@*(AnʈstrPsY1EІ`_􄑢F/AO pRz<̈́2< 0T莛 ?nxag5 oxaXAgFIMz?u#/]K)/Bl6$YAĉ3D FJH4Rɺm !zB j3 bE6z(|yP{)镇YHd0!yʱ9ux/+|,qD. 0Ƣ]e1x&ql?_~:;Rr m3eFlh7G7 1El H7D86rkzQ.z׉׷@pRms#i `5 E?S:vED[J7p3caֹ| >{5UW@L//wegM=%O" }-ۗpL/f?*֐<̑k_507@Y)H35Ub.n<_BC[Tх{i P:k/vYZj3%@@9) Iuι  wٻ\FF IΟHqUi^bpD.ܛ/AQ-CT[( * g%`"E eJ)@L4\ 9`+%b hl4g(J ?N; eO{’w ?g kj_7?mZ,冁s/ -V L62A$Psرe_v~x>,.7.C'&BOK$+ڼ` ;+bgqk@S3xX_7V$HI팉BBPI⾛Wc.0 ^rNqBD[f{?tpIGXrnGތ tǞ[jB[5z{9Dž݁t @1Bhh)4K[mUrCwKyu1 ЗO7 A/^uuX5uҗM">P$FI~ 2"ZH!E*ÿUD!Ҡ}E\.!ȜR6 #C=ǔVj\   l&kiA-P\<\ Ukh2'J E8H)І+; ^n ;/"c C?>+ .7TR{rE -YPp훋TE2sJwoȞKA mN4ZuʒD7{8'_uT 69ȉ~`I#U7鎳saXrKD<U5lu܃:[urtu} YV7П];߸tЃozaλfGVTkEtY+Q: Z!|ϲ\@#>Ģ9\ʥo(|+iAxz E@Fl׬"¯ <&fNjo'Ci~OsUu{N2ltCA^u & ra:v }% 0Sݖu8)pyޑV}Gy4~}ӆ7nIk+? G-d uK?|xon:[8!#D_}`s z }?QqMG!"G`JنpIaأ5@C IM=\•j, Ce$;E9 r]YoF/ ޵ (&y}F[y~m{DTDoty#1NDȜ|$7́O U+P{Xj&!4uFfP!k+ [} Pi4ˈ-AuO+66^IV ДQD(+u&ʤiJ=f6;'R8}OgZf#c9f_UaF݆=k\@lHyukN4޾C3KODR cxD4 ; #":$CfK-I;fk1hNX熽>iukv"5П5 E@lz.rG&P-ɐa=KPg)8CLaX1^}|%6cmo}*$(ɜKF->w~^ |eCPP$EՁy8is/޻_܋?>q mVJ wݣ刍3֯Qx Հ2%;~Ch3dsU1q캵f5Ϧ9*;Km(_tYp}&$h]у@b1Y39}UΙ5n;CVb/yxxxD 'fkڱPP+Қ1 qtL<^y!L!󄂀rOh7VY!Z{R JĽP3(j5ņ+a@Kx|P ?ól6->/5 c{ _m9zs' 3Q>.ImL mI[\E~{_"!ޅpwo"VIR&N1)k+ zQ@ЖQn˶M+|$oC(t='^qh"1cW/t! aMi!.)^fp%C7$ԌO<ӿ0˜q[6㷍[BINnrS LDkk[&pxW7u|5xk0XQ3'rfzwҚ BյM^0\ P¥6@9>>e@L;.WNĜ/ciX T(5 &oI,"m͌?W>P!Cm;;d)R$9#J2Ǔ ڻTR{ʔh cS1(V bXmL0IþE0lnBw-) aey0}ȊrcxDdO @6ox([9oJ^4aïcdjtlj4|aJHfK^ӻz "|YWZ Fb&s"04x]a>d $XFmBSUP(W֣͆ LgTI  cR*wyo!U#bm8bv0.iYnh<@ܶ(h\QM &+)8䘆d n'厛{#[>$.؉?dQp-rPv'sK'O  @^N-Bu}hX@t* g+{{?y(cv<<_fjeDC^SxKO%FU6TVYru EWOwV46(4xr9Љg&Cx+6tG6|ȯiï~p 1lVw{a> gIy]dc?&@gE! 9nRgr[&<׎;&jC001B޳ ry2(_QK)ЫCv:׏dь>LQp?pӤۃ,یn3m CY7;'hA8tA-;QX\rR[ԡGƸ>sN= uVgL0ɅW銑jO²a9Jf-:TsCnQ"uSk!\~!f^r> דcx<{! @Z,b̈́&hn1U<~qD`hCXWP[R;K{ ^5TCZO گⓆ ?exaqo{ ܵ?(fHGCVq])@a"@QB1=U~q{C2"H-b|/)\M(XP X 6bre#fM?cgȡШ\)m<3oi [T~~;@m\atXָm+ѻ*L.ݸ(vVu{.ƢAއ5\@1 HyQA;8Gh=FHC(t@*uU9׍óM@4ugʊy 1?S+eҤŚńXo魡/Јs$e l+#(T)a`H:,[N:,hW ,c9nks=| "P6fÿYƄ( zma3bXI 俦 /3;~:j~h ?nx!gi0<3 ?W O{|%՛XA C'0Ll(+htC(;A! 9>c<@f\jŷOp#& 3& 7Nn%)^PY/CVm|H{tkS\f$ڍK"YZ^ o3ƅiϕ8ڜg޳w.ߏx^wH?8k}X̝ܰߺr?.>C' aOEz?ڽ:"qC8kmdݻmU)޸XiR!fMAFRlCw ;Pg%m'׀ZdlTc? ³0w/l}Py2(V<<G_ [wBfO^ 7_wkVCr^7L2O_0Q&gX$a}o>q7ZDOt $H3k`%b|܌f#8a0V\(rHs;B"[!=PyDdž( 7[07eL( ֶ//_vBkr`玲*wzz }t3݀MT7Pp PT6!ٲ'rxHTf8PR߽PbIfWb Ui_{8P![{P O #fJm^!Zp{H,cH2,|@".+[tI1mٖN Bp)IG c[UA)B6 4v(~ ?e0tGdž4ׯ dM9tǸ/p/7T5e!lZ;bynoIч.7H#!@hc{\ •d  H^\@)!xd&n>@@e 麧pnҺ=WBah\}(ߨ@(9' Q묐{?0u"^z.||#-))j8:;LBSSFdS8z53VAl,:Sb]v,]s|o /ЋSGˉH͊qpA5\gY`h%E0`!?GBӓl}7Ly~^8}a6  uxS(rs EL{'<<<<<<<<ogX)?;BV.׏EW4eC'j#(7P*4@1't a\dd C>ܹ,%8NI?ׯWRΙsZ:puY[D c4 JPpeq.1 GX!<Ò_Ï~vF/s!0C!. %HKZ(I.#o߹ȵ!͠8r [`>CȂNr($仧{͖:m/M©K#9/O#nL#`Mʙܸl ']"O}^=_^?gy ؾ6/;ڏ6r-ǔ w 4?=&7K7Hs= @Ā1 -䝌Ĩs2K<<8bHÆ05bv^@2R@#!hŽ#" N@1eĤqJE?i+t 1tzTB,D  %0A[HkЌx? {; ?g%(D?ݭ,+ 25CH9RX˿oSČv8҃x^}X%e !X'͖͏C𫮎lcrteHIE.P?W< I?&w "OQ 5ϓr],W#D\=_+*29!&>p>#/]+mN$ڷkoόG@9ܻ /QP3bc`IGPy%5pA\<%%tVwZ¥9ni5%pB$ם:͟ בlS/ԩ#@5\G+@1S踞" .ׇT7NR:$N @315Bd_PZ9'rOރbi}W'bPh,,Y€-#Ğ(dŠz9@MYR!'[Qϰ-\J=~%!Gex5o.uL:o_4WEo~p7𵆿f/Sl7Xv܉:pn2̓2f_s>R 8cI g?R`P\c9O|}{JvֆNQ?޻j#CB"0ҵ P ]#J:!O%8 4 9Nvg!Q4XVbwFk؍o?;{ 5:tE`GqX8_1KN! o+6=u*C44a8e6}Ⱦf {B~p!HvwWBc5ݵ]z+8iJ>OA݉]+V9{H-(8=ȈsM%l6gӈ~oBU^4`HQo6BP*@dgDm2ؖ;<<paZhVZuzK qL Hl,C 5S׏L/W[BGI( S"QqKrrf$BX)"iĚi0ύڱF~o/( ۆG5r' +n/^J7z OF+|" 7pKjw'B>1^|ubZnZ_ pF:s@ 9D%m J)wI2ɱ).%څbXmyRkzp=7("='pS9X$p qr" ,Ƴ%Sk~"^ݘmK׏^J!\t}wI֝m]sgpxv0hU1s9Oý~K^k 3x app(-  +۶n]qd-U_"y"ΝQ (j _;2t#ķ$NzZЦ)8̅HǷu-ќ)s [ {h o.&Ũ%.[H!lA،B) P2[Z/Z *5s1+,X= 9R2\o\@h9(ffbq`T|@~s<s~1,;冡_g%6Us:~^`XKĠg_{ $vȸ}!ڜ1wWI~(hHpc@8jwmʺ_\>\@9%/E]ўT{6K5@~` { nn<ɱ % zP.:-L0l{؍"R u0=}T^8r")?|Con/e|%ͽ <ÁnuT_h#7L;qȚ"~!)tt$#ؚnn3c)b=^A)PIf}Ŝl)Mnjg:&*N*#Pxl9w!Yu(RLJg}l,*a80Xr RFHrb+upQGK~sŽt uLU=87͊&b&ZQxk hMVR84e3@7X.vt44ɀN)”F/B BĕD2o &iG8uU6=Nh@V bZ}1DĆ1+fFpqlo*uU:7& [~h^jEZ!(|60ߋXH +mT6Nzl]'eA-#C;υblU p~5B9I$^!׆H"r/2tt 7f9@ԭH`Uf ,-S9LXnd_qzfEŚd<bC.h%A`cÐUP \[+Nȇ%c,::O @8hr1ir =Zה[Hӹ~0HD,ŀ Do:+ PM8 jjP A(@S RoSHǔ)~/ { 7; gYG >'1gM/Cw1/j/cC|rh*l()' \g+r sV\IXGA:vтRNJvBMB7W遛ف؅c".o!\9R 8s uTҎ)bC}]ˮ]2ddkײv=VܨdR`=NK&Bm&!UvX=ԪVV(5fAQ#-br%|.=5G{f&NpBH']2}i  E5A !]jDKx3*x~W}g,Z3Zsz2: Fu8/ O<[M^}`9O[x~@e;ʸL;~ |A )Ν0} &ztf7d.5M8g;<<TvN 4VCIe ɵ8"CS! &4c~Q" j U_=!PV2g2%'"Ņx}jeJkA`R`F=Ĭ ?ݽ.Fxd>`x"CԆ0x37;>G٣D30͡b)ל9(5Î/.±itCL*rL e\}<>kU 8|M YW:A-6bn)^L)B1pέP[Vs^-rYv\duw"o.*c[}&7ԡ/>j! V%Gܣ&d堬 xxx@X`9WIMɚF|B@HB6JHg%BxXA0(*pEOWwD tPԋւJauN>fZ -緹t7+nwr"l@-y @ ` )) XPq)>F6& (C˱bGݧe \$p_ ~~> afe4TL6IoVzCii(A)YAX+KvUN?Tn4CZE8l _E_[G x(_jXsC_64 ‹eX6N''9DNp9tsTXjA6/ +aNv/vY8l*jˎD n@mNk*\8NRGJPCݷhD΍b!HAHM}m>{߭\Ba- ! r}Eڻ,*F}B}?= hnB%@qVRN3tw> >{~AsI:8DBW{K8o}>`-}pp;k =P)8cJ@V":\B4\ H'p?J d3u8h,#r?!bGQ5].T<ʷj9ֆ5{><<<<<<<<+vS Ȑ& ŽZtJOL])s}-==0ZA!2a8i%2 t=JJ0i@b~e8R-AlgmxA$e̊SWW+ WVEBx^ct+XlHx@m(#OKHQ{A/"غ b{n%XD7{c;qeA)J t&J)z⺘GGoDȰw#F>'svw}jR ̑YeaC}{>@>&q͊:=ۄ׃ h#ڱ?O75`P8=oيi#@ A Fgk1YMxPqoJȲ|N_û߷u޼}Eu-YU74pk7"HdH5 .AAM<Tx|߳}$xO (< >z S>̱B,~Fw/yxxxxxxxz3V~^g!Eq, l6j-Y=ZGp+8 f&=U:g~p 1g /+`V`܁1| >ş>RINH -f8BPr"Ы&~Af8+ .wQxQvBf'h !E£C .(rkĒVj3󇄳EM e8W?<rd?…&GcCٵ}d2{N(9'I0*D@m,xZzku޾獂JAZy,_}]Bau+ [!RN Ι~eØ)؜T&$*\kjƯ2( PcqZ\4_e5_+`mO`CO vEȝgmAp:@U'!txF0\HAMAiQ@<.C؋@^芅Fn; 0`&Nr'ESO"ãYD<&Tc9s"= ^yK O9Lz4$\"&. FLpidEG\{#X)~V?%'tGhx' +$`Z<t@IJG[jq6sČC?WSq[8%(;Aa mA ui9jE'q]szй6ڽwNEbݵuaޤ#GK}sX[q@y<'6kudh)& X%uҼelGx|kd\=o3xzQ#Z{ (6 *:BtQL \k7"{(a"n-:"k\`Do>J9Jܼ9YrE5bǞܵ⌑ n9G y)&退# 0E{b{ݙ!M @F >Hb>_;꣑hK/y#"8mOÿy珇<<<<<<<= ?=La\!%`M:חZ p=8Pj6yaȂ~(G(veބ( ǔC*uHQ&gDM],\s/3Z!lج3 tǭ6,O #O22e!H6|a p-cytWgsJ2tZ uqa~g#AW2vEX"$t;^  ΁DYMmX 8g=m`5ˌÐ)8&HC#7ڱ[v D@"O뼽tBs(aq\lqC:dH0c%4K \v>Ծ=Nԩx-o،`م{!`P2zsQ71ˑ?ݿjĄ=E|1 "ѯ%1%H $'&`-5h2^b_1njAB "䀅.Y߅P|V Q$@!;ܸKktOtq`辣J8Yt%H1IU C%J13o|A2ʴ|Q4 _gi1`&YA(Q).ObC)kT{\ RD*Ag *,؀4PP`N  u){B?#şM~5}݆7_AVm㞩oA0_`.$\$£;]q8ҫ~m'?‰6~/\ ®_:o97G¡IO.+g+_inD 2bBq0᣷ǫNa%Hw 9! ٮ;=xh)2"P?^=nuqKָu'^-f]I챰H>DΡ\86},erVHㆀB." {8x=KDe@3%O}uHW9$\_Ev  @oH͸ZЬFL ^k J?ʔ & O;⾌ TTG60tG>Έ@->UpT_g3`z6\X4 z<`_4܃7|{ 79k<@!@^KȐpU2mCpX ;N97?gLr|bL= J:gB;JګBկ: ʼ`2Z[_.,"$S NꨍȾַt{ҭLbNZٶ_bvsd #d20f$D!Q= /Щ(EE`Cj3z`_{@S5a' ͛fIFQ}:,2^F֙jo :v9@8P3WçGQwN-# VN_4gY/✱ FJ ȐzݟyB'fOAA[{[ :Pr i(9l{ S\|\<,*e MA^uL/4*fMe U82["PQUK -bK zxϳܣ @C5=[Opp:&G-pu޹ # k ?# XtGqY{<3N[Bwlialhá#3=*Ɇ9ӌ;|s!lhq4!ZpPػNPEBk7uBt4мa&Szz޲#dA r%{'z@嗬C6@ lr8$ɕ1^n !c-}(uJ0$u4Hήe|s2\S»A}Et 7Y1._Î(4(1O !఻E{kЗ5DbBE:z xb#hC@ #cDŹ3lgA~\%[Ht̽ I@^ol-0+A$: E~h:A_7H?(g3|@ L"0 Zp: xC)hޏЫOo(+а(4 z<"?U8:˰!Ƒ k8"ҹ")0A̮yXbڝ'3cLЍm'9onnZANn!~1 g nK: _u5˯}po RAs4]h׆-3":bcR/DqldXer `9'Aya t59"OR c#K# sz޶GAEzb"JœNQPy OfQ`'WK Ѓt8"sr[$$gޏ#ԱJ=H,%K+b#ex^ xs`Lֱ1bvUXJGm?n@6N-}$x2 WNz 9oWvyġV|1L<J:.e: XK*nmz~D3*WMFPx8(7Sj&PS`BSNDo->${ /G僻^< p_fbOcwx5|b>~v(E#IO.uqc6QiSU3#h YPGIQbH 3j()5# 8FHhyv3৖;dz4±=q6a }33@ p7<xA`#mSliZY S;A!``+[ǂ4D^gx$x`V(b"X(2F+ Q @yVRavb0 /AW F1i f觌>l ?-XDhgŞ:2bE|&1߱A%wQDPTERo}w#{>MPt^F~I If$feQ6R4S43F/;&ovq0YںH -vvZ;fDRtnvI/ha$JtPBg5@!*7mB};6 MTԩ~h7Kjt<Brh H#7PۆJ J!0izxO-/.Ϟ3$ n: -|F;'DڿY4t[v A?.4yȱ}[f@;r+]s)01a9Р{|=W`1t>x<8p`6X@Cj5F =h QxfӚZA$Hܣ ZP?lZ+1mtMBy56d`v['7O",HUMkr}H6z#7șS L+1waLjVDo*2s1_Myv~wdPR% 7 $Ry91l;ZnNh AMY?Y(=#[Z?`g&qw7my 9~y5턪 c޷!HxWc6ڄ&+s*o r jƗ6oG)ܴ#M2vHXdXh|ьq.3SGyL&ʏCrl+UCҝZr Dja I6Mh@ ӨBҍ1#)c1(+`y`[fox<x<Y'42,f@@KQbdFvd䍐XiW(H4SBmVB''ؚD !3=~s._1zQycF_6jFYu<#z1Ѩ3"B0j&k.97;?z۵}Ow1F^?RVqQI0%a^cFFF׿ 44kn4ӧ[ӳ9+U<:k(\77]t/ ~$ɟߞJ?z3ʚ 1oDߍ%#yWu(\k='SvJ%{NTlԉA@gnR$ ӝ]wpDQ }h"c HSh~|dfrD}BYps85k?L8hƈg*哸$RQ}m&̡U<kjC#S$ÐcC0YIM rgM-Cdy4 cPB=(*Yx &{0Ǯ]fN >s֣x<Y/L`F"-08IhRimQQlOٷ`:H + ͵Bh(d&dI?F? g6`0sFeFg z"FXS3z5V 4q5rA`ix(64O|+/~'5O~_܍(Hڞ 1V`>AWcTȔQܿ/* jDoy/֝@ @  gBAzp.)2zFsAE4\7iiTl0;I `(ak#u%5 ˈ m予&я_`6oi~4;sw0ˍ[U/L(g5CYTk k6`胻$9@osip˙:BdQYDLai v#8I^yR.~E|h=_:oAQTbq <hiۇ*.(g\IgLBꮈ4q3Qg<3]hVl޸a{hKij8,ut55\[*?;<)<oy<񬇀4 .q3:H @͜LP>Ƌ]P0^lee ;KK`VǑ ea0m1 zbf>eiy f}Ț[Ge"6 L/şm8$W]t o3A#5v3173>"ey L\_jynz?s/zCʸ'}0|ɓq $"()5~/ؒrf5UFI.bFkV߃LQ'CIp0ko(H&Db Rett Z^֚VQ>E1A +*Zm}ƳO<6mF@-ݼjCU 냈ZDfDM.FY?$ fP9538 vu$Z 8r~ҘT מܰ2mxz_t-H ZPYO D x<YF)tuBts bV`h(o24XwD@k<@* $28C-lCFW =6F_E'y7Q9W]><=Vρ.0}Ğ2ֿ -q(3(H9P?1Rvo`ӥ AsiL!0)g4zTڞ6!zû^P$F , ˈ"@#aeO]c(%Lvq=Ru>G⌣~S"T$alrFeF +;_΍h pf! Ujە"15/.ūX~qVlaT Ù:{gӈh:3uL{vBm '@[iZ9t؎͛A!byAVh'1# \7S"kE Qh3mfwn41Sƪ&_ /@v|p `3zF`4i7w^..]Pʉ^Q~ Ҭe5xxx<+LFq^~di+I>pPrZ1^05bj2"y$x1(͵Uv[GoP_` hW$1w`{ = ڐ=deE2E3HQVں[&GhVpM"ݜo, fgl$ Qe7"z?L1ږ6GC/@>n%aB'wgue~-[N0# Yma&2T1i䚻 NIs5IoC$\Efkd'ĮͿ ?3Z.1xHe^)tDԁG:<>0+E}hwM!z-q$KPE]Qq3h$t*wCs"V#o+z{1.4?$+8ws})6$4]dKT5(7XW]cm uө*IVFLEͥlaȥcyb-m3}v]+k$MY?żϼ{"~ƣGc\-wz] mUXk9'Mi ɽ%"?1燥^Px玹d-F3ƴ1dpL 6'^>  }fsD_;Մ|k s}pjΝuxDŵ~R>Re:x IW&rLtP3ؼO8԰f,Vba*>ڟ)LCZ, '@x55Sν`kр>s ݯ}SAl) -;w>'"Lޗm]ցaa'J4 @ HD1QJ;%qf1-y%(4\~>kH:v/`bXJ,lKמZ{jn;=9QAP}Vx~'A$-[#f :g 2 wF@jpяoMWEDTq2;&-#gj:¿{b]_97nJq92\btsm|d[Bcs!TskM\^F| `~ "CiD*" l;MXQ링Z-wQ{ B@ @x<"TlneҬh +hrSm= *UN;9+ZCc) XcIfJ:~7](`eM9;)k~}?O[FħCv1dy׍1T{y2op "H>ݔsCY 5kLFD$ $%=W 0zO ;]hFY$,Sb> ^}S>$݁h'54DZ*͋9P6ǘ?Ju1])$>e_ge K<X;8N4v\; (0M7N#ЧL͛Pytkߏ{)$k14f@P=3t2$ّM#a|X2Ccl^c3iГK\>Q70*ͳ4Y  ƣ#E;N}C2zGExp |iH#+&z}Err}rCD`҆sUxx<Soj6ǐdd6z:Dl^GV//H-/`]Ĥh\LA$CH8C@՛7m47V z<5FwZ#) &4z5.+Y VxC~ہu?Ͼ4̈$3?!%DwL!k5*FAнoOA]EQ6b껨Ä(UE7\ׯ4Y{mEz"cFKfCrr}$Rʵe)?a#-#zko0_`jtĴa+_u|\'OydcٚfhE/D^0i(:wDCe;A:L!yl.4=\;\2!>S4p{ Kv+^3F1Lȧ׋%dĘ@4}\ϑzaJ GP7n{ SZ {f24,35 ӽto!I.Y3$SеL6 >,?5tnĞfM<oy<|KKgV0  ZIP3Y_>  X :9l:8t(FׂکAz 4?5g=Gm̪>ntԯ=cɦS̘{V}кlvժƮm+X`#Ip ;n$,BrT^36]C }d?_ l̠z#S&\2gSLw~ˆ7FaP^w1,ӿ ܫJ2Yw ' a4R8&R9 ͎gt 7԰}(0; f"YWuf!ُs; ď/B3 z $źǬs>DN+mKO;lVL9#r殈!\q׸7~<oy<zybH"F< wJ p@}8/'f`qny5$$95zV `V`,iνt?l0nqgFlFk亢)dMQFeܼ " ةvۏh='L=\Ͻ+OAb|$o7O610~dLS38[?-~%ul [z` (U%EZ|/AFX䵂@_4~FʊP9&^}ckq7IVi h44u-wRC(oT߳jpd MSuo9kÐ#zc.^ i4BΤyL hD3Q\7]EoGdH8"̈́bη1)(„3CLA05r;:|* ]2z40Yq s Ղ=t()d}g2GމOxx<g" ]3S3Ch(2$:by/dZ]Jtr_)Un|a$pdB aOwl\׌n40Fg5_A=j {4s5x_o=.D1{=8 Dzt㌖|Fu.@ Dߌҟ&+#_EC{?&329l_?dԒ"lg Ҷ"&HEҬICH!PDͥPu5"H@v*ap>1Tk 5!8ٴ3: KG^nj=#) $p<ݯJљ3Qm[SSG>J΂m:ۡvye%C5ៜ3sG#̮c_ ̭]$ aLUf4 `I&+P7l]1 TrooS-@2R]>Ԃ}^^K ,c8]<[eirs݈Uox<oy<v{'5h` %B("XYX)]@@SnZ2Xtu K_CUï2mgчsF1Z+i}c0(PGfLd%`*:d:2A_/M3S*SDڔlkg|V~jj'жLb/<ν }/bD=t\` 7L?JI単ǂ#dvv H`J[B,&$)e8E<=/13q*JPN6ZuŹ3phG;Z1!b cF!#}AHcT~r'm#yzYXtxzvYx%J!©V^ŝ-qZHhw c >?pX!Hlʶc75ysiz7H6!?~hXC[J%{4X~Ըx<:J̪d7kTrlT+X]^g0<2 S 脠5OvvHsZ[G+s`@\ڨ?=:_3d~h74Fh_A燎yO,3iF?g4GΌB+Êp0VV#HK/ HQ@X>>#z1~~+HeK>:ɁoN__L$dX[1CcڎaT"J,vwƌ/Heےv5'bo'gH\;5`Ed9%:oV"a>Qx5hI3^++bqEDOU'\)u}Z_yvt pMP1tUdMV`L{w!pFEs/>ڀK$31$/,ug/Zl35qCWb*]~[J>݂t tԛf[jh3g# Ӈ[s1nj3@ׇbI\dDVۛ@7<x<1T\܂0d@ZDsX[f-5ɨ64$T qZ`VPHX*UE5P [YJӛO續etMu#67|=燛藍~ܨ^ B '2%k7~JHVހO~0dDEu~9jl;* mӼ)[v^EϡWuKkG #6ʘ #ǦLKU\y yK"a(!מ욳0kTy9c(%ƋjGs&RonFc@*?f0s5+h E&D\0 N D5 m*TUTrZ[\}F?h3I`P35Tnށ @Cjͥ=R ӽ4o[ꐣJO Da ǫ4ԐI4p"mK=MHv ĩ4x?"y`f_^mC!+6QxF^C1t)`, 7x 9`ZɢDYIM>sBkίx< ;*xBhV4؎DWsO#j@|Q{K!+X]K7#vRۇc_ٳ3HHْ$A2Ft`|R g,EoF%΂ck yȅ>o7~l4WdH (zKncubLlүTZWĵWxw=ws  Ҧ(8k!F Й>V)H܏ԳTk¤HK DU   tobZI9CJr/s*g Eb׷=NpL$Fq(ѷRbL ;(M6o>$^׃ #%͌dChzeҹRPwԶ[k"d ^Df1fjk21FMOƮzLoL뉼:L$xkH#v}X9 Kͽ5Ƌ@IV`w}PdXu p Ff]Uz:F#* 9*"*S`5(jr0sǛ >yxx<SD?7>1aZjo pEJo҇ ᜋbx"2hd [5,KaS`KZ.Py6#"(ŀ@D &B:@ ZAF>1W949tZ{Ɓ/.w ݴ۷&=3#ٿ!yn)=!x{v"{x qTEq]J!^ &;O#y~HĠ]4zF`*s713 6} ca3i1P-<x<3 gn8/914܁M F(&Dq#p섾|y8-sh80>u4 jYf%NVYIxQEebn',*3sQBm@Z]va t)a $om88~YL=>1hx^ՄFxaj E\I.D+a-etEnA&`l3t67zHn,׏2wʣ98F4fQ] йyi:AH.P-+%#fM4[WΉ@mZk^sEb dVܜ\rr5+_mL.F x^#5Q]NGBUt?6W7vֶV00R'ylN Nxħb&θf*rG 56V[8er}+ oX^%k_{KEut22Gy<x<)%coE' O'َ`59Nί˧"`)JRpc4 /h؃];{=9#,4",3 A3Yrp głoڌh+jUF`R @?cqTj(A!T3lXn5:yPh24lƎxad-WMU3B;0 ?rίZq4D% $ K@㌆p=eI7П.I]a{Χxx<ǃr>vttd͑ K&ݤ"i<^᪊ϭ@odM.cSo& WC&Y/lau}􊑑EZ[vԿ1i;)B[Z@s@zFPb"ZR&tHN<@3{Q"VJ'u \cM"܄ׁ&6ߘ$<}5iFٕa KCAdI>pU_玢h:cĵAid4Y՗m*Fo>G(XuU]֜x#C Du8K'H/ `gaD,]]2D+K%p Ҧ1k]r^KohFa֎j,A@ Z2s1M@7<xE:C'~Im4@̊ǫT(0}U͑3RÁ0E 6@k(Sd~1{=Nv|D@ I=G'yǶN'kqArs:Ih=Nu5G42O_B1G$l}q%TßFn0(`b5_kr}P=# FF&\oV,h /DMqgp&ÈX;FH1qO)sfB_0U\|-Ԏ}2+[m?}lAEՓՃ0s`יBe)D,{kH Y"F0hbt=z#:C2Fyj8+(jWϤJQmA ptlж玐k]lP=@ ;-Mi M [UZɽ( jW)NDh&@r?rn݆@{%]s+bhM_h3׈rW쁏xx<37 Mtxz5H&Eq*jT`F($ՠ7{911GI #I3 I6RL هflMX?j)gPG:Ni{jgI@L'05аFΠ *rdA.b}(cˑ14(JHDY M}F0!-2yct4Ado<ІQhJ[OiG> O/wp"b{`͜or#i(]0 PȩU,Wfx7< 'BKq12)jec+h7 +Axu4WDz:JJF< d*U`bˀ.-is FDsM0d*JwHTo?XfomLXSF] }{z] &D'gCD0W1C7Tl aax<ڝPF)\II*7WI:ݏTs:oH@$S͉4uJkA?XCE7&0QDMr},+ZҽC8`KePFkLpP=ۦ/TmbDnb~J%u3hLMx I*|cjc5HL QvU's]<1D+5L\P]0D3p>$eJ`p7<x<Ϻ @`VC[ՎuOԣIi& lTK o!*lTS+.0{ 8vd7\^;#@d)^VZGA>\5[M%\Ġc'@'OJ{(A-|`3{2<ԽlEJ7 Kc}*7~F~Gi}菽x !v5]VaNF9a kBA_D )3|z螒O'z BsJn@rs03JP&M7dr||@W2h0u59T@[db_l rf\88@x<ߊ0`EVq۽b3Or]m{uA-&Ȍ m К?3Q ̞N\ @J ӵsmK-4׀SGz@J kRP1ۗn['gF'p|1!#0RX2)B4cW]h{cFyy$8okDg$oi#NEx"{c"_q\N}7\jȞtnR 핸^VD]׷wy$!eYd_}wN̟ (5Ht ,p шD}L /b4E ,l*_fa#i}VW:d"; 8gRr$A>xx<+ }!"#!*UJ6.64o߈5 v HOj>Ft2{V4qQȥCp8tz+ @R @>9 vcѱ.//Bl>̐D!t4[+M(Pi?a*2~f~i`f>eF asE#TE8cPE8bA?B n<(é6F!ԡE.\ Zɵ[~͟ae[N.#f 1R_G#y (9މdq@uö39J3(;l=sW KTJ綪>_ aJ)*^Cp6OJʮgj)Ș{zzؑW>;|#ayĬ`XQr $GQMA򾨠ݱGC8&1CK> ;A_hQTVsE3r6aGE~#}BE䢖:z2PQh}#y҈)~ @x<&Zut~Vꙅ) 2"jB!H\8d§<J`I^lCA2ruh6 avn{{GF{ PD: 84) ) u 5LܷT3 B3Q䲙#hLxX F_6z=]D7 ݡ-Y#}݀ %q˱FX>DuGA؁7@(ْWf? ?'25^Lgmk gW3g`PDOXKL3Gsm~W{K:Z'N>.)ܔH!E ԿL-q^玟4z&wN4*I6 xY-6UWӿ %l'BE:+6y3y&kz6]dr󧸯v'EX,RW@7<xAc%i % Őt&PPAk1::W_-`'n71p9oJ$tf+F`hg  e͞NAw Z-&i :ֆ늿u#+*iѽ󵄱(sݔx9DȾy8ZNް}GxuF etQy<)̩"^*IEuTw HzL44^@Wɧ TY+-"SD DPe7Ame lK#9lv"F€srNJ\5#XH38_jQ0( ,VOTojÒYq#p#%$KirFJ"K^/k~]W~NΎc H#\WtˠJ`aB$8l?"|u60yemK ͟seůp$|s ^xx<3yx1mlLB0L׎r gy$Cm>0JE%PG^;xU$f:01-_FOchN oiPAgPs1ȬΖR M ع " zc$, y/KfE3ur\ YdUHʍgq9;4׫1o\  .4]J&`vmy~=GAs{meE?7F{֦%DZ\j5:4kVO,+#[\na D"§xNrL}HfqPN\(oaQWZ>x<H.ݸ<9 }dt!5~9lә@F`Z I8"į:@ԒK A;#jz<H+aNFʏ~ I8 P4zr&4M62RI#8DaWr5;o$5FrS !]m10s֒qb0Lv@=3tR_iGaӶl8jtq[|`c R@P- w"^|[\DP.VșP. (xԎ@M@ n-ҕȤҨZrS!a_rV^Va.ڗmpșg)vCD85Ν+mzHG+2#D;]l f $KM|$4hұ4`<54^k~݀""#h*hHFtӾF.rC ྷpAS1*ݿǍ_֧k/ G·m7<oy< wux:G,timVG: eM /"!I$\Kǐ\<9֤Gp*`zsQAʦXZ0*՞TAD&ki63y°qjs3{:nu2DAH@]s_|ڃjC=`R`pn~ }g]bU'oC.ьxag0oHi44RҘ"r}%jt 7BmVfǡpkD=sL&6w[Eno#8H38? }`7Dj;%IzN.cPj1I㠒}oJ)z1#ƒ7rn,"tC֑ [E%V>B$zjxZjMQ+ *$E fŦSV l",((M0bQ4IȀXR!x<g=FaPOEÇaysm? K!+hF8- DmaVLlPBj53,fAV*[OqėAlɩ5&PbTj +}|k@3MQc蕤\&SGZm}q&t52t}лQ]݋PZ:c6jCgpzKssK3b4"z%1!3R.1PF7LltUxQ?K׌Aõ|#!E)aZ$Kti.$ΫeB[^p@X1NM@Ҷ}`nb#͍7s`;/aHE5F6H̵c;gԉIdz<\=jΙ% &G9 ?Yױi_/X~X\^k!m p1i3J*7E@7dT ӭOmC_ uٞ*@@7<xgjgʦնÈPэEK##4"p鉍Tz=lx@=FPrx4bOے]RF*d %(21^w"5ɟΫploKb=AoA2WAGv"D z^X~9|6m #bDOo{a?`H?[>?;>NjtzxuڙlN cH{2>ٙBr-TrKs 9[GMvrߓH 4`yY2kߜG<3_?FԠSz<TVO=CcaX$:Fp u ZJ=![h7נ@m$cEMKUG@VrEj똡ʈm+\/}Q4~dMkAߪ|1BBrzͫ?/ɛ@!xQD((IL$dvg݁v*tVP ;a6Uu>zxk øHf8݇e4'Xeq<Z}0[|ن;ܕf|-|4BߔN^Jg Q9DKYݻ Lg?= -E< Vk^ IgHyHBʨkJH64u?BN9{}}&W%'-U-Y-.|TZP@JE2\'P^N&RbeETATmADUE2d 0 0^8<]H1\E~uuns3u"0]d ֿ|t[pC!1߂dPH@@oPd;'@%ҭ1ͣ/|d:WoP CT#* E D8@Zdn 4nּ )a\H& det?!?/b6%(Br:F oU+_,PKy fՋ($rSw5H7PsUXrGcsE'^i ̟t5=IDlE$F<#*F]:)x~SsX[7q9*^YrfK2'*yB_K).Ix:KMt6"HqDT?`֏ 0d0 01X B b펬O[ʡ3$K9LLEp?&W {#_ž{fM-w޷g03 &RD1DNqUq'q9%v]YrMd\rےH !A2}}OS}: ,Xͳ}B0t CG{m[J/xԷ_*6A 1W# 9쩩-`5ixYl6jǠHvQJ3?z)=&y"gO?3̽M"Ap5+]FbˬB}MAy=ρKIK꫿Nvgsڑ9$B#${lIw9 |@!y~>|Dž +Ң&,LcX$Zy8'O0v "2u$PgM}m,N'{5)d1;/bQИI(ܵ'ǯwPOܽDTޟP<ĵ@?s3?kYd2Yd2L&@u- MOFՀ"Z%V [4eitМK2hwZPQ !]_K:Gk#+оA{fZ/ 7hO`n٭m`oZ;c6 Y)9}pi*\!n->bsD)L0'<7J"FBH|8’}S /wO?!L歔ŻsF8wqķ鮥&kfa3Eeǂ]~S 0`ߥF:mGGPQ+um0Q ȃoAZKZ/؋c_9Q"J:Dq9{GD"B!%̓(QHRJ3@\g @u&MevF+2nr9E B]\ 4b$hJ A a\L&L&d2dk MS<& VX-P.* vy=YZ@Ad]^bǘKOo k#_R\ic<}sAW+C4`-BexhjX =0N4`v7v1k IckB W^gqž~4C|d2> =݇"S#I$M(h;Bu1h):٦K4OK&Vԕ>ECVp,R'_Kg Hed[O'GTEC<4" Dw6;&'%)+~A$q"?9gtL(ҰpT-1@ a"zNvFbd7qs粄]qXX8?ޖPp!iA)\WO;ԝn.d(d2k.FC)umB5/Ui|B>vՄP[%0A}5xzsiNScs}_z<џƞ^X^Xpv`63[:~/ A hcU62nYj¢?o>`4 &^drT ' EX1|[Q#bE@ѧ<=D$w>O}s5>h'ߖFt q>9?qep?nfѢpx5OC 3pH|;%:a=A\cbR<<^٤Ww u ,B\r YQ/dIXTQ;#sxכqa+EskQ@8u"P4Obz vSz' @L& @L&d^h COPB\5/}2 =U\îH 4TxUbV̭x|W}ҁ`'\;Pn=C4RśFS0c@LGJ=GۇҢumedG7G'9vh'yAc n?L-/Lf  6[hAPc(rͲn/Q_Zow>H nL4D"K8P.{@L+0@e|w 墉wӨ842'pWRDFBQR>oO:ŗ=tR.ȵ= '؜6s^c /d#;I (lځ'I*HPw*A(d(d2$ɕ~ōSG%;b8M#گW 3} A,؈+#Z= N¨4lN]? Ǐ\P~e[P|N4 إo2_XZ%K{ /~xwIR3dŧ(GpFD¤/ 7sN} ?-]ޠI2P E祁W8Xܫ܂2/I~2-mU^ӇO#K#'3BF(Abu_F3ƯSI Yb7r>a?IK:zUT܉TъLl)O0y%4!D8@7O4g|#"PRh@L& @L&d^/hc(} }P a)樰rFʪg"_[H{G9tp#z #7.!j"&וBgs4,aFr;7`~+1`@FP~qsm /E#2ۤ)*KA'nذl) }M /|(H3~P?L[Z!$Y0ґ!cOqq9~ޥi/?&|XL\ DuzȏC$"<$U{ ES{I)xP"L$F6rhh.(+(_ߗ\92A:\=~s]P}/ # C #h\ +#x;c4P&P&d2QON1XN LqJiϝ8'KG:\{9tj eՃ?Fmq xe5ȨoP\a*XWi+G4s{;Hq%@X!"ʁğq'w20OVR V2L2L&ɼPWU-;6mY meGy2_<ԊG"P?HH `P'Π@"{۝ ?n7]-ob/a].߿l&[;۲3FJѳzx~7zIYW7D&0@[G%HAqx7LNߏ+~X/5`rZ~@ BC'im—w4%sa2rb}Iq- CL|!'X#{E4`J*ON"'tHt xx8 @dd2Lu['FwJ؅;]Sdl).P<TAJAe @k[(˩h~!j+Nz "F\ ƀJ<꼀@";Ae v!Hv^* +H_ܪ[I(-8;HyˀF&VBBg9z$pvf1s~ pL7m&?I,jL̅[f E!?S^oCW ;?̤ R-[©o>_8€VHE! WϏ_cqvc# %ۄ(X|PE ĜISBP? :-"uęs?3NR?.'Q 'IRpd2Yd2L&:@.6[ v"BYoe~,ӯA;Pk"';9G 9 Wn5zGEȴP|myyHK )jECأ=ZnnDj^:ʊ b gW֛>x'!ńųn"M|U` L%\7Mc9 qHLu?Xz~w (ʈΧ;:BKKM- ᘌcLpEL'0/XY$WN4> N"go?.PVsm"^LTQ "NR, B.!h]/.xs"{"6sQ=g=7N$J @dn($@:DRRDPdd2L^Y~S7(PVѸ9~} O: dh)E.4 F>-6l=yPmݕ.>ѓ_u| a4z=G@ 707&ecew@:˝W OX}0BEk0b EbJ"C=BqUK'fd2V H]p(^)(}'wC9> #B6{AQcH(Hm;tr/t r E\GA:ݿ:L7,J'~L/'&!0Q!T'lkED$$&jID=uϕɓ?$I\{tbP@23ǭ ' 2!#"̆9x"bJ"᫩BDJJ{Osk2L2L&ɼPcϜP :5MS߼p`gC?wSA4_ޢ(0*ky=BPYJ y8ra0cP s d\cZѧEp0ͨ*zG]GztMlKoiEWqէj"FQľĵ=~Np%pw~x!3̷F1]W'A6bmT;N~ v;o};/4BL(u÷A'~{G;n,}N4tTEB|Y5t$BI2XJ-$D`ux#I l!M87~i7Rfҡ&ID++( 3BE GpD~\f;TI?1QIv} ;2L2L&ɼ>P˫N* (ٴlm9Ĺ gz}VeHE٩)?2@a_0y txȇ><>Uoy#NŞI 2ȝ:4ӟtvf'V\B谳eqUP-^ׯplX>3wR&B EJň jDq(9g)rL&@`2K1?t>nL6?E{iVH$^KȬMq/\AY( d-3J(i[9yq)F-$ߵc`E{| .y$#Mu}~n$Fyl҈ bDk*Cs8uCEqEH;~4 @L& @L&d^ȅ~>$VbZ6N}0"@.֐ahS*:o+[ ˇ?Ƿ v`ogZN{PŬ5 mF7@sn>OW`gsڥT=Mv67YXޣt210w:W&YYl ^O :i`?L^R,M(~#"s((tek[s~]tԻt3%?(Lױ#ҭ;BOc?T2|x\*RC_[#ќ8q玍Z1J<=Mcw<1X+TeR ~wS6E Gnvx7w/(j3R㉴un8zmQ֯Ah,x^ոk7TXxch8!|_|>\CPiFK};G$/w"qNhp?dd2L3rgC;}Zx*{s vv;z#,rȰs;ܺf%4o_j7 Ad,,. oͰnNZd"&SgmxM|m`]~b][V ombBG) 8=z7!(wdz'#d2) ټcFmT);ÀL6~b-1 yyEwDF8yOlHW~};ܝsH h[cb_W5V e0Tm7ZUō1&s\Ui&%Ua1ƶ~UuhM1յISP'oD۲WMk2ƺ9UAX1P‘ "bSZpN' qpj!"z^`߄Ynn$OYNx% @L& @L&dAׯbL&{;^`doGpp',~6S'}C E0|@ :=D 3AX7  sFnZ x~dd4O<3oMmuumm100ȠgKrh]=gVx\[4wd2|C Q}.A.4|dC8 'JEpΠK'"ǎ@L& @L&dQhQUk(~r]Ѩ`4(vJS3/9 sXϯQ|eu*Z2yaN7 7 XKUA!9x`{mٶeu)syyy5"ބ9| uQ׏G 4Q)kX "` W |+L^ߚkXF!*V6F9+{'DvElF]ȷ&3LfHED Y_+3$4I_[|9{\BaGŢ%-g?2'͐vIPϤ11sB S7#ޡdQЊ%u-'%Ft%T7^(U5S6n___(PiKڶ;SPí;)s^J*$ IJ !{91$X3G=K(&pq^|; @L& @L&dM O~1(~sرM&#a<FC'Tpŕ) v8,,qP/ĬAh ]*-\Xu[RwE>Hq ^Lbp='aVyi8np ":{X"\R{;?b \s5 x9b4`j*ۧ-A]q~L&)D>2K  Io0 D ; ĕ0kka@ ĤP1B/n8 s9 FPzbg돿ꙣ;cNM+<>5BciEFi)GV9:eE+Յ}º.-­]ޤKqBXD#ŜƓb?;,EP5aK!0h 4)h`!}0Oy6s u5-X\[;gLct!wn^~?ƙL&sza08@E0GYKu@$n5~4v͗lA uu51]oiKy&&ĝI'ChROEAI"1dR84kD(L3KvtJ,.^ٴu# ۢ'RjR۾$P͋?:dP&P&d2{C^ܸcEfмg&O@{;aC; -BaqXX䠘1gnQ<C46RG{3.R:c3O"t`9.˃V)^npH( >yTSh@sۋ?rKlm_hXXrn1w&o\lp5&Id2\h@$ rDrJwt%P!t^  7_7D?<ɐnxrzECZco('8Gj}w^hGi,U 7=DN%(s2&i.k7ߟ0׏ ׶K gdںX0߷,-4IW#vmI2~l,PP?|# il AVK?"ɼ@~S@U:.ŪqblV^iOÖY L-\D$Epp 㱴[L Mcn5m 1|ۿQ rbPxD"~>֞Ĝ>1/7LJ" oԀ'vs}P&yҏ滐d2L&މV XT~U=lԵ0 W.nh' 3D @(g~> st]A"A1:AmD{z ɴQF a kN2ģo0`_Ğ[޷=5ۛq>-/~_/GÇFPyX Bx]ɕL&9 rW Ca. E9a\]\4zxg{\U0*6+kκ.'%:"qp߳{KDcy~H~9`4+X74sQzqD,U~B|*N %}"NsMN%M$|QȰ7(V z\mBYYphyBU?呈)um8.@pGP_U4}j?]Ǡ߂_uO( @LMa L&db9<;&Z8HQ~˺ @@n``8xEzCG>n1bT**DOS|ysyO4HOgEO<2(zh+20DA Tym`ؤ"Շ<`lyheDxݩ'p&` =Aivj׿!߼1,_ϛQ "BH phYKV1d2gǯt =AD]@3u)1a.!kХŬJ4دϘ+]^9}/=,TحI9Z[LjCUceҔTEmǦdy8?aqβ0_3ӸHǸ2A2LH" GP0k$!yv+vE`߁e-ie٧, F`:oqʐK]<&%&$hkj |q:&! Z-4}"]BGҹ|]d(d2̽IpDUz|2η%Ucr4MC wK 9PłŵuKN*8(=xzrc68Rvb_I7meXvGL~ `G5ŭۇD(*Fn6oׯ7ns?ՠ(3LU/uLKP %9aq;Z;`Z?Qc.pu7wl/[j>p,8tavSϟX ׇqs%w6+*@nJ4 PN{Ś1>Ęvh>-zCEJO7+L IAi̝7Z !<} tD0~_ VWŹM]]\sM|.m0D92L&9s3$ `m2|X !,-L56wVZߞCR;ǽNk6CP6M?(TU|/ׁሄ|3tUhl%"?9\&L&d2o.pXE\;z9JsÞ0+ۮ zaN^+AP ~գ fȨAṋVjKvj4V+ssK}| ܾzJG0^x2rcd ~cqi0X%[ /~0ؓ0kܶO}+8n|ܥX[%/\`wP`q)QmzU6pАyK@\H瑉 *32C!1Mx8dJ=lg뱹GDю*m:8!ݣ*x0~t/8GitV MSRP!|녹ğ,e2,e2L&y0(G9'S橷j$MHo#a22gкJƃo{S<<3N8쌰p]Vpx=="0aZd܀kjX,ѥ|+Kj{ S?"~Õ Mk)~9 7PŤ9;Ͻ\Sy}x~x2ZnUL0j@ݒ^`셣ye%$ Prc,ʹ GO15X ]m#?e|%,km^ek oGP ^{w~ 7v!/"Fp:#jI0oL&O /8aTQ'ܼaf DOfxn]Pљu~׀|T.߹sIB*!گ6} n amk/b0.)Ҩh-(MӖ"JYX8'w) 1GIThc|=BY? K 6)M­iڕo'!-iwP%ַ*ѶA' x&UvZ'^?$T]PVmqxA+9\&P&d27 =BĠxϬ/l5j7\V3>Pݳ d4[ 6a4۩8w⫁ A~+IGNJ~'P !0m)>H(u@:#qu*8 v6_l7Y9[h+mHf 1cM`L& @Й"CćCyE@ܽ/g|fBPGdrc#^C뿹x? +/1^~LE#u=py#_;x ZyU- #i.[ʦ}x};Te4XB R<&ᦉ:I őfqdknT$]G3!+́ckK\@ĻT_,Gc1$ܟWI B) BDfxd2Yd2L&ư C(QšD-L@/qSxT4-U&߻ ]zn,N)*앜}(KG9~j @U +9\{~8&3m h 0;<~!nYv7o1??5l`gc9/fUUO. yK<@ ^Q|88@-n2ډd2!<]!F<;]EE";R\r $ާ0 U_n:0Be41o>wQ^4Z ah8 V;L󁋗5\_{sѤFM$DF8s| gP R 4!r'7(.!6'`/JG&~Z>ʜ'ʈo1ߟB/_I> h"#"9$^IP&?w!d2o-=̳q@R4u,=^>E/GzakPongNPUB t*P_v+ȂuS!(Z3-ChЭw zJqLm 3o({7my"-yN> ? j<GC- ?/L& @#]%ǣ捋 9AH8#n(:D> ۼ ?s÷w;"* d窵/rsc)Lhl0Ƕx![F N,N'$J JAD&L."ᤉE?>_]$D9nܕ%E#(rO.0׀J(Va<6gW`CGz}V8~C# m rV|k0؃f6ݶ 1Ax`ON~۲v6U @h{cť|cn˯5 FLA?V: Ճ<8"[0d2'p h 8B(cf 9:F?b-~ ^ȋs O CReŗ^x"rRڗZB7[.xh0\,11 4*8"Sb]<< }lՓB$!`E$_'R.,^1/i``Ķۤ(L#< .a ǓYbOW'egS{mUe"" u bsvm p)VzJW7a: 7m`gn]a{re |oK[vh"Ulk," -*]R<0Z{Zc@4yH*bD0Q2d2@A-^ُm@jp @@\)n]TVğ7.ͼsO/6yʡ6\]kA1biqfw6 &~n-&9טdE JO!$=.2m8wr=ySGhln.P[m ]¯|=FLƱ I_phPJqT?L&d2L&zp"LAgm GJ30w~ <@^'J>;=(|+BP sh8~_ D 6+R ğN(8~ ?\Mbw~/ .ʕۏU=}se8udw>|8H{DnΫw_pD gArI⺢y$=I_'$΃D!ڝ>_Q}D: eQZP O>rvP'x.E\t8_w#;2Ld2L5zXӠ)P~`o813[ձWptYA0-f@s BX\W+i)P ڑpUz}7UO5@(:^Ü=>-Iw*CNşT oӟm-dWVƺ7"3=4=c DDhQ:r*/n:g)|k@W_$R Riy:a88#t0[)5g?P (O@.^OݳؿlOz\j,z{yƍ>hyKjʹ7! Iu$y~s!%Je Y*/8֍n8BQ-(4:/EquV]6u`uCFC w\BGizm!<~8rZ=0ĸR:;خ;qFRXcIAh޾|:@B@&JPMx* L&: H\ЅFeo 5 OG |XqN13Z^9WX-ԅP |yyh!rǏ!R !p$IȽr&^|@Ax$k:OPȈCr٪Kav" m-ү 5_4_Yd2Yd2L&Zp8 j+]͋>^!/+h;U )Tw k<>yI{;u+mlqqNԂU#@xm𹾥%vԍ)@,wA({Po~ݦz2-fZ`<7[|;!s_3\%m{{7_.Q mKDTǏ|}!5"A(8AԔ >Њ8:6&d2F!<躀5h \1O<! 3Jx"O}LRDQʢ™p%vMImx%My#3/Cxnq$3 lFE<=*~n-q¤V>UZ *Jjiać}~e:_#B48d2Yd2L&e8AD:P D !zUw MSH-A-N,2oѕqsT X -Dt ($Ĥp\ΤXz=$qLWuLy+z/=H.t\QnLAVVG'/ӯw6.=Q y @ _L& @L&d *', 604h#"(ڝ@\< P|qhsT ;vl ۿ>Tl 47Cx?93\BaFD؀r\շw⏀)f.2ȕg 7XLU#')4.]@5|(}.ԕ` l{Y&6BN7O&W7¼B ȏ-E;׿vƹ im('os~.Zce+l aXu&kJ 9FLO}ºMB׏@1ų#tkg"z-,EYٚ|tzM @ "z#|6؅/uմ?5[8yd2: O (t1.EEH\0^ B]@[?߼ {sEPB;wxd='HӤ%.uCI"7bJΦəDb$|ݓrȽO{_6vŪP0r_> oj-PDro_L&;2L&ɼyGx磀D A^Ӹn@L[N ]\@@-ƀ4!{X-x/Q@! XuyTDo#gvr9>P& H` ֗ƿv1='ͱ<#*ݩE`D_# we>uB#$ ۧ酓D̝X" E,\JL.dęL2L&ɼ9"{Q,x2\CmXx(W@\^jkceGᅳ %֊l\: AsgE̵1H\uP-p>o-%,`z4^VUDX_-c"l\T `|nyqD J4fR[J&7N`JKѲ1 m/ IPM!jT埨 8Uc?nDE\ݞ7aTݝ/#zvgOij} un@-~⏞b2C[.P&d2Ϣi@4KB.tQEb!Cq@i+]5Qex@ &S/Ks{w;(8o{[P[߼ AµCI Gf%d2ZLFMG/#`\)YZ O*UcQz ʘk)J)ԍ;:<$z_ETU'%$HJR``+2ɼ> @!ÄyP c>|8MܹcVyv}NwIq -6<>%IMZJPInK~&b%>b+%uh uqOrVsU7ԧKܠԏ &d:%/. [.>,d2L&s?TVx@h7tCZ='%J6hUec#(6ε)f ޲lPX 8A e ACߔ.Ȯl\ O+'/ phDGm7ck/0ԒIQPtjS*."6V8fZ s8tLjB^mH ld27PhP1(OP?A܊ѹwQZE+{t+-Pt : a#&ƃ` `.VopHkFukl_dnC6Tk%Vx M ˃@gZAK AS*n_L _49)? 7!+.<ˤڄBln &1$4s'miAD'> /, ǘ&fv:NqD>euw/GK;qB7 ?):by"` x:e2&G>g!d2냠 C W"\:9ȍ[5 6"|~Jٳ{ݵ0i RvCaS0nqMqx_mI@B],a@ S[wְy . c|O€"K 5T_WLA{K6\ߤ۫*.T]6&]@8+ E c%[vx xD\Х嘀 "ͲG.>U2 (y 7\/u"Eѥ 3S)n f8"B2p5.XDLƛIzwFS6MDąs u 񈤠㥬 18wcxߏt"3ԏGuߧb~!e2.&OA&d2T4SՄI<cj'yp[(R,wlmY)u=L޷{W+%X鸱 xFEO7m!Cn]U8wzY "y `©as`:eEP~|M`w/v"}b>vEQD&ugWUcc1EMo,̵Hz!vƣwU#Xbd27,[_G7/ NAߐ0E M a?GP6:t1V7s H_% a Kx|"s^O(~4 wCxLƯHL^br?0<ɵ㧹B6şL&L&d%mTGmMwm"DtմR;#0uK} xR67Ū-TUܳoVa:f4GL>{K%(^X.^xq,NLS*Ja{vϙSp򅩠bϷ}\1  O^Vd~SE[V<0FDau|ϡmqŽ6@dT<}fxE M|QN(|)>{!tuT|?A@_LD~Ss$71K=An ?,p@D-P-LЗXP$R: i1s !@ ҈pɇBWZTMsGlA(qB0MMGwps) ZA /L&ce2L&sm0wDQ|K)ު`*Wm61^1Rc4DQ[FÂLݿ Jng/{by_sr@M$Ů޳.\nd d0#}nP4xlWޣz21P3\pKtjV[)K78= H,sNƽ*1&`Eu(]@|f2Axp AqO[q(1m)?.Oa_D\"K\GB+-I1 җ?b~[A6$Mpe_( >g2(d2k˲ Tg1DЪFbą"߇G ;cM |ܪ~aqQw:eEU;p*Jcwcn~C-1 bcQ?H`שuU˔=˰8>31 |pGsa95xi@bKU>7g_3nԡ孢UZ-KkKgWvQTő.m?!+ @BpE׏cPo?KDJ,(#`+8\@>-c?lr0r x"X(!`}(88@0%!/-i$n> s6;y15"@E1M&y"7p匱7Ŋ |'K[*d L&dXZ[; 8*rS C185Ƭ`l3*5Okp`9yZVT^tz5ݞݮ)ZPs'WW%"u]/ؿz 5ܹ9q/D}B4tnTZx'}DͯL&s?ŵ? -xg`.T P=xx&*,gBA/c]9<_""֥a `#/IY }L ڐȉ^cx#~D\X\p|? qIinV3J6Cfd2_ eL&dvH ,rBg?i/R\K# o)䅠A}yfcUUUiT ]][`bOe\bw-0} On V?<"Z0Zsr5v'mKA&C{5hM".υ snY#e:cDa>lm%PCqÂ"P (O>W=Ƃ>Aʼn1^ Xve%?LVD\ 0GxTpyT60/L&s 8q c܊,К)>y xhʔUK''u&RBC,N'±me4*c,IDt)RMbMB f#Q+rΦ5qIsݠ is@瀿 s7 8 /d2)d2L 1z'_U<C I#4ncxH귴q @ȓ-o]CwVC 0[i޵o4וOm7` ( 0n+J kpE8@@nЭ:|νm(K( (Zy(\P`-s#ZA7?yܚc9W?WPkboC+P)".\~Dk`OcKXAyeg?;oxoD$ hTi`X4PK> d2WS\x[Hb) '3 E2L8~8#|{Upf‡fºx<%6gaK?q'h/)Ԃr;'p'm{E2Ld2L@d HK/ܦ4\&%~L)y?( uğϨ9_"V޹w`Qpj_-НcHaWڌʼnM\D*ŏ ?k6+ڿwHؕO*HvCeY1>fOud`sc̹j?noo_g pvǀ=+5`kZ(,;nsf{=1Tڒ6+Ef_hҋB ܅挺]G(CT_ޙL0.naƃ^Ɣ" ([hB}f,O#nmE qθr:)r󍶒!5^$.$\抠? ˛qFrgmG@hL&xd2Yd2L@d82Fܞ MaȻR "B f!o;䙍E*-b-w\2E| ~o]ku+8? EIꇗwS~RRZgU~åMޘ捁V{*:ӴSPO`;ε^(Zb9A^MŞcd\hdYQc{#._U/Xͷq"U!E (^ϟ>U@hY#z+nC$j?WڑKDqG"-;/G3.~+/UiMS^#xډ@hn .⢎.b2)cQe69bs7BA$1/6!ht"A rB`_,aW31s \L&P&d2X-6;k*b sfF|G '/7R'5nVAS%P pX#V[ ɰVylPCQ*U!(@]l37)8,7(Sԧz- PuM1HPQA7B~; PU ]䕀2ƂY_kd \VȺ["W,=P>y=-Tc8wBUs;( VTl @ۛ~EDUt*i m\^W!h$%x0^?K;@u0/L& %ڽ7MORL ࿰]_`FNRAM(FxQBOD,MXшUF ,I*FO~iˡpl 4Gξbbu6BN}`O8j'%d#ȳd2G~`#bLlFD틈qA>R.LBфx64q9Hs67qIFqD7v/)qb1E@G [%dd2̵b87F]:)qM@X!~\Cq@Q#riR-xl&cd$NqY uF,X @+w{ǼܧkV "ݕG|@ ǧ)< /nf}yu0~Ό y 1"Sޣ+m99p!JZ1 ԕelO\MWִ:JV'i#⟟\ecؓ<G_ÝCAaoG/&D8 a?^8DZ8X 1zn@LJ|x8[syff3T߫!@)QYw5c?5-lܝ'&Db8D%0&ݟ'.PIpP:FO|$9V8''IDAB ~`Gڂ0h7{~ x1U2L(KL&d^#&(4\L G !W #PK0܏k-ic]?"Hcΐ=[ ĕMk ܍0 XZq;v7/R~<T)^1Jqr]iڸ]isnc Dzㅥ#. YW9=xuj#[9@ @PCu{WmUSi'iX: na _ޔ/P^^Yq>߃IG_.vXGqǕX.4^ 4C~~g2|l+ pP0%ބXe=;BwfiqDݞ֧qwm +&2qcJ\Ј+0|ݥy!.h$Q-#-/yM D?WEsm9L&@L&\ h ֬q†"^HfZO,|l;R.s3hX5Th_2)롇سV@,y'/UE_EGk{_I06 (b aVsj0Ύix.|\Gsזȋ?"3åkϟW+SoݹYMdS/WϪs'hب?*4O3q=ՏT5r#^o7e2̵ o~PG Pv~rY8?ަD&aӬ؄` ˕H+N>aYDzhRVDyٓ Y\UzO!#Y2L(d2LDVP@ʚqu*S|}ܥ;~$b 3,eT 7p꬟z`ˆh#^NXb1PP\Z7[ųK{a~Hɷz^w⹍6 (e~٩..'A;mL󴧛]]iJeWv0F. >CF(_/~] S랖osɋ@7nkCRwCEg..~A%L"!"a*17.f赑1<^Qz`0d2Wsd P: X{iB#m9'&*|FI>&%D;x5̑`~ldN51a+SJ{@ @wK&d(d2+CX9j;즨kԨOlqZ(ш'xP-AAQ|I.Qh`1̵&mDQbRgP[xB[⭀j*m>-it{SR+u`䐳wOֵ@>#hS1?﹐m3x_;2 jǝ#r9E&R\ﲊm&AS Y*|yl )I8d"YLwE'tzSWh ]C_-[ѕwv8߄tg?M*\ .4 xso B͵eQDg<Ďh2ZPh ㅹ`>׋ TK8X` ~* ?#F>kwU@1Dq#f&+Ll$ɐFq8܆lyX!c?@"XFv{6GT QЁolukvGY " yHv I.%8|(C{1;IϬAr!X5׋˩A"' zyp'?$H 7Q1;zLv[47) 5i6708?>OTfGr(7" P~TTTT@rt:B&lxP,å{|2遵*E9|˄}JC@<c2c- Dzr'sO6"+#z8 | `cumPEEEEE8D'@P@A1D&ko?tOYo_ǑCׇG.'dAQ"&3&Er!(9D%A=(\;bHR&t={vz%l!7 /1ty{-σ^3(r VmJO=kׯHfBR(cH2GIf:(XGr(ּq;҆@Cnɓr9+/R2riW[z 8  iDΫYA0 XMfd- qWOP04h`w11dP_Cy![ţ|_47)w<&?U9^5Ȫ/M0h]Z*****TQQQQQSK h]9q"I0[O4#iGp~~ DX E>oPzaJjM~UE\?5Tr8|#mz Oþp$Įx" .mfB(67{[{1֟X[pܠd?'}rǕHGRR$ Ȕ&k f1=Scď{~#aH`6e_"])'+ k?PEEEERgWG*x!T E" VV1J _LHACA/S",/6cx-OUdo]Z*****TQQQQQ1@db7ڃ8D6rědRD WFyXy0jjS爂 ށ0IN[ZgFKi$"qf`>;0Ҝ $c*lS蕢S{^: #7:dno̔"=0AO-zr=/p`:\ۛ vѿO#xՎuK4NHAi5|e?")b4p蔓"S(y؜j%Z!m}M_Z?\^e:Yp****P4\Zhf@X^ƇŝKtFpS8zH2!<)qbS)յ@_HPGA3}d )[dŅō$Jƫ*7sn6]ס,yfŅjdW*5 T+tQ.$ef,lk֦^PLXM d@UA=\| cAp#K ٢SdG(G#p`{[n] W`usH^3M"./™ki#%IC\;l#"&,K,#j8kвF-eZ.12xy]')˭$CAzTQQQ} ͢I:.> <A)!G LCPO)R/G*\#CѸ~J*~LH8+. 씸/~9| H%sCSPQQQQFC( GH Y=s&%rfi Q.c FH !"O .vXV.p|YDN.7P0%xY$5p0Ic$ؑ3ej"! v'v\3 rkmBGGA/:Ba'ywv .|!{,. 2hwFϙ瞡iCfO=Bـd"McfA$`J<{-+,׋ X3v.͟F7:XAAmDdrՃDEEE_DR>1l24&̩`J4V&Rt>dr1˳%FsҸ:gp#T@gGUUTTTT I X #>"( L@f;hw=vmxV*,t_3GEALVaYX:apPi !1B8{zPAyy8 \>k #}4F$?Fdx3Ag4mO;ilmP8\捨k\@-usy 6y7#dax%UwD݀ȺGɛeTZWTTTGiʟO ʫ>|m지$ԕTHd?#=|)y5ط}h\/^^ϻsn$@iLQ'uﲢ@_0H% {2go!GFp1$BE3=UZNrd&`זӗE<!C}\Z'i8pHE%-X8gk~y8<<^ͥƓnH)Mg>D츶2M;G-S5"&f@0 Ψ igĄˮIⱺ ތbqzC1C$B.8,y#DKK a~^}\ !)N\l:-YpXBT0FH.ޕ#0+k\_~LQ\"eODʹk|ƛQu}PEEEE;D$@94&<" ^dmZ# "X^ QSh>#JV>CO@3syv .WK't9Cf4M4=Ϟ㍏1H BGpk ylnpEx)8\8 ׯll6HwD &'_T?LLgf2QMu63k|(41厢xDbU:,">"s9W^Θ2 Y=[PCFƒj>n >$(ɯbheCר#>j)(wJU (eq ~v_J ;;ցi& {Ai[uyPEEEE3R:p?I,eYϙFHȇo_63a y<6c-q\IxY bv(fA%Ʌlo̽%!J;i(:N펼a#&.3 DP`:\:?'iXǯLi[~n-^R@s׹rRG;1o$kay1ɜLKI"A+odH|2SpCܠXHqdi[gа|~X3#)pJHk檤\) 0J7-(X4"/kGy Jc0#jb*ߗ].U C= (qC @_ZͺTTTTTIH.`j'#ZO>BI6caNoÈ##xUAbs6]3*kGPuѩZwkx(WLe5m4mOds`dO\jG>f:ͽ딧}f(l'E܎)b1lmn7ϬEZZlՐșKD"tDq& sw|$PG ()p >rܯWjJnr=ˬ ii@74S6U9A<$3"K7&8~y&%¥skr;rO:G"<[ ԉ ]2 gSU똪+fR./7[0"̠̝ 0dzQzirP%)8čyO@X:$?1q@HZXZ#;dѠ=)86""⾾o?XCvaYGjn,#rTPEEEE%*****>ߐQ# vAձjċUeLȸ/!Г4wěP L9T-tdYkK e?z($,\[@PF|{v NjϮgd21@$ ls"!2:Yӣ&Ca /QЂu+lqJ>Fɲfhڲ ;2ƽGl^A 'Ӕ=z`A|4r FCoVOQ"\fĄ.-ޟN"i-ׂRdNAE 6"ΑHn с~@ܸf⬯7r?Ğ+umxeOY byo Qr@w)r#_MY)l6T#6<61U-#L~2?ƛzrydhEn0~Hlo5lockx˛Fw6&}+ݎS,t pӰ>(6`c V [D䇵pcL,-wL!ӳ3fV 358g^q2G/sHb#uyOk=L#/,zc!K;y"H0.GB^byU^% گ#hꫨ|~?p6ڃK l{xSuxʈɔ> iL,w,-ϙ.L:jI׾w?ra//K:FhJ}Dyaϟ`dc GCTU{Am|Vj`le^ؚm}u`~ VTT|@׿ w:wG~ e{# {/X =bahnrv^kdQyN ۱6OO L]PA]?*PU@x"'#?lYڑ7uYE"al6^$7^7iYoXōޕvsIb63[ܬϾGMGit c|& I&+oO'zDb9!d#>pbD "n]7vbO G ws/ȇGdti ӞӬiZ݉O @L]|j3<9{ wo{߳1~a'k@TuoBCu!1dB#AF(9jۑAQ=uTg\fk[{V!%( u 7q!nO-*_b%x0ݨ}]qI%藁O8yA?n., u*****n6zh0?H{< F԰6xmC47-Gi7pCVk+NQ)j D$ T<ɤs8?mhS7w$|.Fl'f[N0%T<{4  S94r'v O,:Ȟ$23sWg_~3fgQMve2iw??m>_RuA݂SZ0H/ï1c[C=捘s1")M[AV&YQQz֖Imsn,  HA cƧ"%QXď6dJ-) p>F$8bQ oׁb PZ !idɑ? Ldo&&PO_j7{[?F8D?BM9R&5YXSaO[;a\X=HQ'~pkNNyN٪xD#zD6P]***^XzP55@l+`iWKKḥF e VHH.9xR+-+n\X2CWUCBH=^6iCŏ'rߠ <TVAl&ܔlk;;/qk^bɠvCz vH!~<)k\!)  mCXS#e_>"PA3xՕA3, $@/ Ifs>KueSB*;oY!<+ k+zXpkx}QL^|7Ml$ ɂ H'(8iEE<]Q8K_*S>|lN*ujOx8.K(ӒXSVnTNDy.n<`A]Q*****TQQQQqp h ύDhN v#bԑI'OpXJ_dV#5"T[y: ~9_{ФSTA 4B/9F]e?A |ۏ1Y^kn ![?˵++39_Gi[%5f2VWo?5/=0rne퇺LÑ97;,uca V-RD*KDt tS<'qO9ܧ b񄳝 /$@W+**nMhž2>M{T|-0!Y_ bn9oP1!vJ>  ˊs"yefs|qy7Q!ŀe@XHEEEūodPor? wq!gb (J=[PrdY47hͤ\4\Nvu E{&O$=GhS)HH,\P%@f"n/{9u27 ̓pa}\O>OvmdOG6F4'5ӴCܛ}pZG~u/@M:Oe)P0Ŭ3+?w@[a]THrdk#~r^}(V `հOg{^JH @^øIDЗϛx+k۽J%h,B%O;&5ϣ4ح}07L.BMһߋ=OF ᗀZWJUTTTTX>JȤg (x>F̎'gzL#`uJ/vlq]J 3sC?]ܜyHM%2~}q;L|ٛQ3џh_}ɧCL&-,yw0l@ ȎSwػ?Ǘ/q+tD7Qځ@;>&eoPsj'/e@FMKiFŠ"-~=;Վ4fHLFǗa$~!=Ia/5~6φ\= U@YFؗBK}~[#%OѴ!TDꕂ +LV% v -+3Un2 ( b}{ դkEEEE%*****n>Fv枈|_eڦy' 'qc-@ߎn,GCOȍOݵkE{7z/yp1 P"h0ApL o{MIb\ E_NJO3FL5͐FIxog]9>}*J!LX}C'-{6 Ϗ6 b*a7R>.mk%Ft!^Aݜ70/^] ZMTQQړ@Als]L©=-:+ 3v}#,v >}}#Ͳ%]P/eصk@ggϓuPEEEE4?쌻ˇ?}VNi&&$p~c=.}XPpF(lUМL!Kd&3 爱O޷zgzvf>Zٚs'o 2 P#ˊ `{&|gn'_q'NL fʵ+6t^\as}QHJjt }f 5&@YD|8'Lh(ۛ3}__}Ꝡ"bvɘo?w3G LƕTyuЈ9$ Y|4La~. 6]+N-h?x (~`q!~5 *JT&cuP\,;Я_¹"QlrR%O2?cgg*UGJQQQQqBEEEEp #~06"?YO֨ٸE3P~5l|Z{< \F?+9p>1)$ R(J 5ٗ2ӃAs#Klz"Q^ <> 'XwbDPb#M2!ȵ7J֌agYԇ)-s:0_ 5?"FIԿ |![w_ L&&wXiʗ}y1lW3m뭜k{;F䏀%q Y?U@[#('AۀX Kt+X+;[!"٦H䜀U@2*Sy2N 6gvuOSEXUmm (84xK,Yr*j ^P90eB?(Du =|o,K|>bkҙM<O]bksN!@%%5C= 8O2bH2i2$?s/^'ҋݛo4K\{!="s$WeI,S=1 z)dB@+F-`9R ϩ ֚wJ/f\'_7?5xi8"ǣߧ · lq=~cO%KKk,lV`x GAБq̀Oա qO;_.-OX\Ջz2n87F9D&@k''؆+i';m2X'G6FO+8a 5̻{̾ <x,R9}'(w7P/YdO& oxw=pw,3 g9{.s+F옢 i 92hcʟçI91h)?ўMn_yl~`)8@{eH>lCq=$s9-Xq4P\dEl(%>vhSQ$>zV,롽x2MqA P 8(+J>col뿢&oodAp5$ 8)W\4yeB](/P7`,r~<1v w- ט/ظ̤/h/וq o aerH 8|+ͣY_[!bo)eGV;~^_݋pn#<#}$䂙BPA-ZƓ=Ƭ^ k[cݓ?A2 e$[Z22瀗x ߕĪcEL.. +IVT,w_L|7 #<([QQQQ C)Љ#{C)+,47o;uuWތ'Jf"+0(<>˻M_b#~LeP_ܸyffT>^@5@#{'qeN[&&ՙYANz>@Yz]P&r؏CӁ >.lr5yp'dxp; R$T[6 |lMJʣ`ďշkc.r)|=M|p7ߪPEEEQ dyʉtK l+mz15P:¢KKDC\ *6nDnM{u3T[>8&pOPٿ(DnIZ?0yb_𑽈 g:+C>wRNYy~!9^PL YiRda:y9LLB&&D3&ikg{gsc|'XWz:"d§Gr(2䧝,|i49v u|U= I Z~wks+::wburTA@QD<Kc"@%4%8")\h֛Ww' N0pi.'#$9bߨ6TTT;dr0 do-S#Rp'ϏF$Kp8PDGd{<?/`Uȟ?qL)}.]s鯨PEEEEū$pJte"KG"pCFc͙%Sim\G ;P6zc?AoZBOPFAaԵ=߾tmv؅TN^CZ!ƒߴݷ{ls *Tɤ ]/d?1YMc$PECs&FaS1fP_5?)1@@Hr>}ƩL>!5>;rU ĕG+!,#Fr  VN`؅EJ,jx(+=)t|YXDROB3KekfQ}U&A ։\]4D;u`QQQqcIw~-S\ Q[o_O@RC~)||bO/U*|z?n^V^O}EEEE%*****^1(iAL7ʈM&rxor.t3씝1Kƭ!qDͨ糵yye+9&Sψqv5"ZeuL[ҹ͇Ei,4[]jwo /!)?<413qyn23EK&r|Sk!Ȣ{VaMa&IФ>1GGzG &5g-QhdPs.%rX0OSRPiy(^Mccj7g 9e5 bCY Շ^E8e$]"BH;04̀5VTTT@>ST "*? p A1L@Y^W6lc ( $C4 ߖ/Nj-G PT E{ ?xp X R+(B @Ew7}A,@ս Y|y]91nH` 0?^egn\}t2?6x$ ?tۋIk(:!bP R =]NDN ? 90@r^Ģ_ %9Pc 0??2S`"{DɰѲ쳇Oo+[r 6\A+Xk>X#ݩh?"MdZ:U}ZSٚ1\lsXPI ,<3_SSxXt?~C 3f>,maNud(tfډ 7,RN&1/yijeG,i,='~:}ڏ]>:wfwi2曆nb>` %wxwowߒ;(P質GSt?B#~r`!(e'.2Fn SdX|C9؃$%E bˋ|j>Ͽx[sN@>gW3K8k|r5rf̌`@̵%AF-N(97d t3~f6D\s2঻?ǮM*1I0ç(:Yܨ1$gL-B xíL-o"(T;\@i+P&vZ6$nbɟN@,7x>xeh 9ěs%"O\h1 u=>/"9j 'y,~<k˙V -_!<d} N/OP&}|x(6Ou&YV;m'@>=}ՉOu ~xp0"!XT !vzY;Ӑ\c!!do@s5f _\hO,a<_Hxb>,aK`}&C#i??***^}h雁?LUDއu@ 3w4+);z+r>{ }pMI$C03yȬ zT{$O?OuEEEE%*****^"r  7@ PPD\䲣9m|;9ocY{ʼ,֨ȍ+R$YXc*A>$Ł.-73_,ʄT-=}RO,mbtoU 71LjwK:yɤ Gj;,|t@Vd!#LjЮ{DZJB0O.}495??z`xds||gOJkzJyk>_v4Kh݉/xǨ/n~%&@Gme D}3,(mldwY\ |tHOW{~r(8EQKko!ǏwҊ0NwIHԕ/" EpBATO i>^Su****^؇r; lS+Aox-JǛbJOX^%`gFnpp>g2IOw/r=#K`J"K@*&$rZܤh&vD%%=bMī$C,Ҧihp ݽIulh8d8̛S_ě-/HޙXvUg[U]SWW]e7'&B&I PSAJE(< Q"(BHyH)0:`cbhUGRT^X u%T\ 8W nU)ذ`,e5Z?;Q{!O@+ {iv n=.6o=^(mS 4" 9׬srt><O䠢{ǮN v+,F+!&:?g%%8d(6NuNA[ u"(V3qK/4%yQ6tmbsfe>I"'eb#F"YNI^8;yi)]ses"I6v)O'|{V\[sG woG@iu^.. cƨa]ҋ7\NKGi:.BM l{ÿ+ߊS*$4x J+Pk~JrDW@ (h =;𔕃+wC&YfA30r"`NT*RT*pȜt.J,*8æ|$o/.{d sO6¬Y}{X1P5h_O;VFQ(XQg?p2CƯ]5Gh]Уi?|=VUЭ8b/˃-][% aχ, = 4u >h;~ri>No3ˌu%}}j{N[/m#1}*ߨOYf4Cp[}$:,C6Ĺ~Faʡ1D"m/Kho#&V&.QT/}xx0%ArN&<~U($mnLR:"b^CЬ< @GW)c`hɚeͯ[ؿp;bK3`+ }HIRB C3Y.{ P6 0CrqoNvuY JI#Sy=?>z>ͬ6/vR (7k7YDMUHĻ b'C/ `"*^4.V9Th)1 C@CiWB?FϛJr[^d!hJ c,?߬Wqr/pT 9M`tYPF_DT*J*Jm3;3OGe..^4d8"NѼJOV&(rsPO!Eo^0 ?*4yã.'%o/^C)@APz#ܔ.p=DǸS+bAbDOH2"$' O {}i*2ĸIsbcz2iDQz4:CYT%TvA=gZ~||w> d=)0pp-caޟӱE D#)ScBP\)H8AV}}@cRU=1/%\? /D YX[5/BR}(|((2)Լ8Pko5DBUJ9MkN@ ZT*UT*k:0ܷk%&s[1٭\PX>t~b%B!|S0NBj` ^X.aEhHL^<6zہX^PueOOaYOϱ.Y׉b2Iز.2WԚ\93m(xij#^Z(n1}|?"9Kx>=9(VSP ^~l}[\p˩8S|Xo'Y?,1oG>V܉z(Eڝ#ʺ*nO ?Sn=5/GRy}tL>& mi |j݋9CNhl5x6fsTT*J*JZaXF3py#RHt]2v][|6s\ _;-Z(V!P${mws7fL:7s廓;|ȷ5hלH\74Źrޣγ.۬"cn]- UA=gWdZq&'>Z>=>Er>1A~Ccu"N9ح:ie*`NS]%{;sDpN޾[2~A &J\=)z쇺<C!;X7q6vMUʆ.KPX* B'V&yoY.{ 8U5z̀MrY|>Ʒ7u xT뗠͉p3;o7g|c0_ t9Uj\\xy7v4kx1ێs0/ ҉ [ޛ('o)Pl3u($n߉<[m4Okj\1[5/BR}zh+2wЕ\o>L''@xAy12 t$5M_ ϗ"jHJRPRYnCO{!b(>?sC{v1ehp}0 pBPH&_yTa".۬i2G7ODL^?5գѼwDi`ͻM'c7O@4Ѧ,sӲ($۲((#!TX %CDлb(X$cnP.l%̫zirkÒsAC̑H$t5x-&l{;# N3\}^`݉DMN  8fg;-/߮/deQ$fI$FbJ'cQ "ב6{N5U*O]x$_.AKyx/g2Hpޕג0A  h-gW/wJR!*J 7A/@o7u5S‹>>DSo8L#(_?nHxezEa{IX?K[|]\et.LX~`|ϸQq$ݯLe@9_M@X(\ٴ!3Ŭ0+{2K@2LGG&PM*+U:˞n^;yݼ#۷ ]oo8+ ->bڇ{MC"ըko *H px/xpT1"`j\q׷"UQ5 ?\"4tzxO]:XX@"S}rz􁻁_f(p4E^+hP@hc~`Rg~us 浇_gAzLWo?TRU*J*J@Bu UTr/V x^q<wAґ"x݅?rOO iCṖ!2*ZHg_9j9?zPFv'ٙW>C1)koqطNBUQڳ0]&tێl" Qt=m۽'H *;>Kj* DSV sGL[ XgH!2? \9W67;\'CQhjhT)PL'2h=D0 #NJ g@>z֙F츲o/u dHER#F4rDa/b 8>p03_*ȃ ˯ύ5eQO-I2ߘ@"$>j: $AEk5KDc2z;-O|x߷5:pp?Mr<{ # #cbYaJ`? KDUMbpyDxΛ*j2f28  N1~ډ'z@#':~_s{'*-PxL|~{!u3,' BPPP(. }}j1ɧztP5"U@}NnxUAQzL{r+{.^AWجܑ^ζ q_8pg/z- }%UqwG?KyQ.s~%ʌUrQ.'uJNj|y!V[#ߑ(x>Ϩc􌐒z71th_k ~WCO=?~Ӂi\).LZk"Wn?@.y`y3諀@IOUf8d goR\Mrp /1y  XI`On:`BL+9[viC M-s5F BBP\ \8)g+\z%|mxs޺c TIԍՔԴ]8GJ2(y7QFOhZ|'A^Ji4S a_St'H1n@ QvJi V/?רA:.@֩S7/&I23:&Hd!SZ"`u@ڢck{r(ki#ˉ\DBR%M?AǁP8s/ܘT_V7'˼_X:?1!''3)0+kk{n2yN?8y0#G `.>BPPP(|PӿaTȴѼ j֮$[!'dFZn)IҐ,g^R|\ N ]aiK4`/X۸쥓20:pckL©\6C{|`8}{RKB!bN-O(o(vNdCjf2sR%Nb:fښ;Ԅ Et,(c:T $U'ˇ h9=UU*\}BQn%<'6oJv&?o;gXpF?p[ 1^ 9|` L MnSi}wY>jps:}1l%lOsf2sGv |p;w, " Bƅ]Zd\ IDbJF',o傘yQ(h4Q\+3B/O# }Wb剷?go@Enp#睜g~)>W{hݥd$_X/XGܐꗾU(Zqo% pN}n!ODd<$ȳ81Ͼds>HiTpzLLtr19Eh))>F 0y7i2 %~@}6W9A+oJ1>q2OM܏WZB nǛ_·0')ΎZ}`޴ _x gڶG,r2Wi\s9w0cѕDa6L}ia6;=-̂Aȕ,'a BBPx{ك 66 R`wj1R QŹ=_E`K_K#Kp^i월80sM&Ua'Zls+/n}]^ xC#B[%W\+)xj{n0i aڇ*Y!RQqߪ$Qfb\{fE/rp޳Tpyf-9>-RzJΌfiyPr$"fmI|z zcJld5&\:&ro.UI͙(O'YpZ`P{ 1iÆAM( ?#(8l՟ki \};yO|D} rN !l uӧ dq"^N?ۜV ]Fl;\ uX|"fVV~1OmC=ωy oh>)% P(* w p ̽9TgCU >Yb+ cd@eJ}6 qװ^A:=wL'9G^FzBBPI[..(tg0C+yCg Jl `W{g}.YNHhO4:`g;<:9{+qnc`wco7;Hj?l#3"Px(玎%`23J@ %ƨc/Q@^Wxe`l[~2%+D%T`2>B:rRKQRkUGBQx{#XQxq/_o'w@1}Ћ DXԈatX{"zgFVIi7VWUyjlKlDҗqLJ@jo^ҹDv9]c_#e;!$ZtXHO21( 9?$ߓKY,++Px޺ >NwkJ=OWMOrc]-E}F]z"Z_+ P( LX0p1R%O}:iN.y(*|yՏ]URuch%***hy-1%Yl, O{g?7QWGϿo#:D-ci5A`'5|YB(Fee$#?mipA}"ZYh!>@F:*A\Tjm3 G CT_6}~aoM|~/-ڣV n{x7*LE?:-.5UwW t;lןy{pR\G1t1pegv'Gcf=Z |X[Q=BP BP(.AV[?1O<a4e|6~G%dX3o>e]s[f _$X&'f8nJool|O޾ʜU:pC~7Qb[Ω/RCefWxEa.MB a$e,,VMl;f{0(Z*%!Dse]nӔW̕9xW=A]SBR 1s|D{,1d&tH\䎎%$}u˸|E: $] @Ñ}Cǧ+k`ܢʮ&;[V=%cW(+gc"B R49p ppwh'ReE gǁzjtD~tG- E ¹#PN>]{&'1Kk`+ ^A\`8 j$WxDdU^u$A9;#?|߼vmyxڱ/{S*yxU\#)~U,?VSN0B!q~KJ.N(coh+T (U !mZQd%d"l*e ;dcAZ0ovB+9j8syDtUsP|Oc7ߏAXhNv|HHJ ^v&!rk]   K?pp#>n@Ȣu`7 Nv=t1ە~P(T( ց#@%$5eE)CtIGe4#NMypqnԲuѡ`}Xם_o/^oǷ/>|}xc4 $c}n  ?LY=TF\є[P*bH*픂yBBPM"L%r! 3%xE?:Β <$`w!P%B3EO`օ=@ x8O0QHfmF_c!sN)W'_[I |>O PXr9XJBᴃ]|xP(T( gKmIpr|r\hBUkieYup3(K  蚾)(2ޤl*чOlgS7?=ecpDңrsxUe~ģh^PQ1Fg)U ܄ 4Pm=kPG)3&Dd.J⚤jz劭}`E2@#wot_3`Rv&0ҹGv֙| kK5{ɩ:E' K Wyrlx1%Xz{&$2ZkN?P?o+c 1JV(|(X3~ XY7{z Bpa?ƥ&, eJ(\}AEREP>CwI'z*ꘗ$Ҷ'bCqIUDv F]NƷrx:8_MGL`s9Z+c F3ꍙI %#h(ω=qd1N2_5PV*IRk%:!}b.Á!%ޱ\B$WWw=RYQ }Xg1 Q S` De;#-b'#R}q̴u.RiGP7)Gq\b钖rhBwQ}RP sD)kϹ,pM\ K?Sek\mTGLu=KMP(h"n?bx BP BP8s(l U$]Mшv=,OyHu\@'I-DO PvJ4-&HHa;t_6 qp]zށ0ܒi(!\F:gO[&4*R |8"=<_]W(JT( g0~EZ@"IjĽ=uL/&Vv7S),N5c29ب~|gY!kݜXjQO1Nqelc7: } ޺c Z 7ku#'*yn=`qOz'Of:$L.vюS"qdc,'nj/ʪVXu>:I dqp<^3s]P#b|)+2՗p= MD[OxUcaQ..BljgH*Ö(9ĬCL'T)Q,09=,36U 9H\ 죏'Csi~R}ǫ ™?T(Z 81.1, $ A:)zpLvy3vL!toZN.VQy桤~=(T^Z?ϔ- #Q&ALʩ[jx' |g[Gz1t儥4T()cS=}8so('IE #qL/OλҮN;NR)ᵰ'xߦJ>=d‹ z.ۻ/Dw1u> !Qz$99ͣ"b_|I=P닫$Vj^ńo H9b_!ک?+ g9f(}n_>NN_oP(T( .k!,G~: oPDb[H N/(ȫ `$QG#ڝ8V~"Um?7߀>cl= |}O;MDb͸_q00SQOk??rlYPS 6-ct-6D󶩂*#)'U9|J /pv?o_8)~9pGI,h+X /V JW( >vcv|||_- ]HIQ&$R3'?x)o@QrR~6iOJ h~0: ;e 6H%FgqY @X( }J^pBP BP \ T yύO+14r IˁerAYҀD9ۮ`hCKJ%XP+wg?7y9~w_mP@3!xH!˜$% tr,SQT6gP¥Dꦀm%3PM, u;j8ܨfG#T8S?7{VB Sj%ׂBp@]P(dA`@ɒJ<蹧h-Ot2L`rߓMiyŲ9ApoU׃1iPLGhY(؛[<6gǭGߖBPX8՜b$SdRQĵU~bvouFZ##r\ۡ)9BMoU)k:@!2Rh؉E(A+apN`/5ZiBP BPxppl$<5 ^yn}M]gHF $H,&5TrKxa8qG?RV:Q}2(2[5yZZUE49<*Y*9 |>?-[pNR,[' B) B.(y6$kNM-ԏ! .m~ icJ'?to*\7#β~BZ))A>13o+Vzi/^N>|c{˗8K E K?neIzLQ *(sF1)+Mb59FB˧vM H%}~Wd13륤y¢}9eBjAu9 l/p`_}p#p-`qBUbïeZ(* w0%;'(I+-'nkb r@Y',KŦ Tb-u7CW!2SjMR>rK4TEbz.J?S>|N|_n|c T( f7HdSAIDix2Eߧw?zE[1U=nk=sd׮NwIY(?Х,[g ‡*P(X9 YUKRB mQBݞݯ_۹`Y/I'~9(|y kSۯeCYG?ӎAWkC6}ElpY % P(,nT_fH_Q8``Q`fCGYM:tt Rԓ&]X{6h#k#\kqD3eOܚo"*e4Hvpl бEd{/"&31?NV __bBR  a 5} 0 xP,?@lm70=L 5Ptvj+[։]W { >6'g, E s&An+Ĉ$ HAZˉ~%O 32H ':)}\ukUV¿{{"Zr =z;hQw)T? @4%.O1׏edO煝\#P: \\~ꍤcOTZ;T nxxzP(?1 |4'=X]W(* H!݈Gr\4uEW\OHHCyQ^%(a,5B(HAEjDЪ6-ԦEԲ\U ~-lZ98̓;jly3s|CY~ͩgkɄ'/gwFd8o⅜}RjɼzʵF 37O)9ԲZ+~~@.b?gGG{{+si S?"8w,Pķə)Hje& ݝE7 SR'X=ŏTs-*)\ J%9HRM"nNzbxsՕI =Oghݒbk XvsȚ0BЄ0ņpϼ`rZlRg 0ˆk`y җēһ!ہd\Dnְ-7ZL[&gB8v$|#O۷Mm>.~Tڀ0_s{^VN}DD,~<=xX8a @k][q_Gw.vn@Hw$EsBq h%c{H:HՍMzENH1m:+<:!8jPE wVF %NZplvsIiQp2IM:-?vv9:gS["%Lx=CHM"[޸_XHCr>X Y4Y+>M& B&Tkoʧ>;wVd!V؈~y̧Zb|bq ѱ;(׹;djN?E/FY=YQ>O j$ʗI(KIz(A !ߍܙ{V s5{13{}~s{ڵru`lډ_GK͈=P"/UEQxAx´wh^ϵWK+ hrQU5$)hb ?RVx [b~^=^:Fa. @ x8F" \s%x\/旻Q%'kLC cg Q']6dxYi57|pm.=CWHcV>=q ^ OyutT+`I (2B ply!D,/9f .Bh/n6WIq!e!KW#VBH cu^9Ϣb<Q2g5E#E (PAP X1fQ$_c^SЂQKAxi#+HA++t-/ jKbgNM-jKέLZo:U+nqقo/.#@m4vg֞KQ+1l 峉'{ㅗŋֻq^EQZy;;w " ܹ2ayQ*' @J &19}$#ژMD)Y\-MC7B';Zg>OkNڟ,hj,vzY8(   B `ӄ*Ks=R@w=Xh|/|K~,_^SSz'~<7)yl=rUb9}wr7ohC.u/> j5Vn"(JlcMיYm{.&=4>>24o=7'M{;#8>4)`o.8 =c1n;>{)Pv (A9@0S1a߃ex:zh eD{'tIOҔX~Oh+oB 8|x?&30^|~H~MD`f~P'淪r ۗ!{eʎvʽ$^y71վMF?ϫBއeQ6B{e^ C lp$! `"zkeMjɊjuB}=0K/( x w53vw.V^!~ "M߱R ~M> OhٱVܼxgՂRx2VDP̜eZc{O J58PB e8$z>Q"wY@B5J&2i蜀  5(ctݩq9_@̫:Y@~މ ؆hNAp(iֿlFRA/uEB RQIz@K}vs{ ,ǢBSk%N/??'A?mFʾQT\U+A%o>]/KBUSǴIb<>;6*H[zDD,!blQ#jĂ%OQc4K4**"} "j4HD!4¾};s;.* {<3w3̝={-k۟oS} _ҧ&⏖䖭rڑݧVvmڄusa'g!)ѽ/Fw]pLU^ШaqU}96S:םI:{։IWj6MJ8rX?.<z.OLS^yG_M<=>l,[%+V$Nii)[-p=XWDSAp "DXIh Ԝ_0aEvWBl:>ME|C$潨&:ĵ$pU.ltr!{bib6㹗1|(=f "MÁơE@65@u6_?jFY#Q͙S-Qɾ1>'*H 㼐X4QZ}TZo湰 `@nIs{>_ќWvn^gS$gO{s_ @) .,vmgݑ¢Eyg{XDڵk9(51Wgͣs19li.ڛ?͐CӤqcu|~s/K b̘G9K?;'90ۜkxGXt-[ كq&駟ǝzf^[:L3v +jŅ|7wE׃~l87K Z 5 =ְPs6WCsz\Sa"y< @@ P((Ⴂ)T`]h@ @ ](C L2TQo/ fodȲ@,(z\}Sx+f9Ғrjuf[<kJ%7gE)"ͷ*Uڼ^jlȊQvҭ.\ %),% ~~[;5bMٙvrۭcԔWp&|Pon ufdRN9\:vl[+hĻ{=_@ØEl bd#otT03G_є+AHO''[caԪ<.\̂())m۲s6l؀8>ÏN=3u3;ql+6#]kwhN\O5Ի~& :^@̼??28y@F'=5g:6Qc '[^U|s @O(6dxZ*@ jC1 ىkiPNv {BȄW]Ķp!$CVțT(m[n7Leav}clKWnպ2Fr+u}7Ά3P-pp^ze{-?C׵^CܽݚCfHqh taDӼI=WްYTd@mCXQ(9& - T3:$_=4C4JW{TG|,#aԄv#n6]6y< @@ PG tE`uh@ @ 1֠-Ar/ øbAb`-G֓#@s@?-)䰆 kqI~kJW!O119/"P!}#ˮBc]CE¼3n)HPkȸY:mܤZ^3@@n:鄷ɻ`Ĉ_ydQRR@yy&Y>p 9 q9眖 [K{<^}\}<xC⻝ SwPvk W)4|o IP/D HGЦM+5jX5/:|rh -@Wjy_@.4A  vC# R=אfr8??nIܹevԫ ϸ>ɳnjru;:&0(֬Yw!rѲe 6:6wH+/`Ej_jDl'XY!* qC|ROq|tsg(>It"r.6@$y$ɝ3G3goAMɳsrw@mB) m\\(%hVz_MSl\vӉ'imٳGv?ff}rȑGMy1ӴW]&|8bp::$>_R|7p;_x§y ߴdNa'2SU(HB$s UJ܏E1*`E.+ s!\Dvepc[ +W,[ܧyfXvֆRXs_ȦFD((MFb=w'G3Ϻ:QiD+84C#N]+8A*:tb[+:Fc颏9!l9nЂ"n}O?Po}b㻟 ~ pGe$ nClYkו3!QzF <38=ztKl,+xMTHq^xp󾿬>!R@$ A u t-v>Lw=aAXoWnVZbǗ,AĩgVzxOascl]= 9^+ ɡ81DIhn S jo,jVz3J-%WbϲIk|kȥw f~ݳ _"Πmߊ&CĊ+ln"ò]LظyӨ 6ئI ߅sw8wy?z/pOU.g=OV-\|(28WGly9u |%9GT%&DB!ngW<4װEM\+0>@="Sԯ:\N$%/JǮ6&ܮ rqՃ" wB0#9w#w_ŞV]zc NX " (j&jlZdRQ^ҥ˪+WF,\"r?K{w](,6NӦ@Dyy9?nh`#NyR!IaWDpxE9I#8HEZw>ox<\ ~|_ņ|Tx1}D%@q{&lR@f?ϤB͢PU.%CO區~@[cƾbTNmȐ!bp͵c6껭UGLeРę>]-">f}"6@W`w"4a <VHKDvi6FX.Eɰ]DFA1~Ȯw 3Mf #at#0d: a qH"+Ѧ"xT_{|fbPl[N5Jڡ䱏DQWp&T DQfFrw]]K.O|G.hb$4U%g74\I̜, :K<˗Ԝvʡwޙ'$I2`(мaG^&0̜F.@f_B\93!DnP X/+XF4Z6&bBYL<ھ|wuuhj".d$ޕcS2|R KV&&V".]N 공b×iҤKQqQ¼=䝜; ~ӹL27=z 6h޲O)6t_brqnh(H31#DR}\lޭ(KPPBJn@cRt/Y!vR]qPu.DfIF}>ƆuƱ„S['$H(dbŷw rϷjUjHcO3#QJ|5%{b` @'f͗DT[;ݩ-؟8wy?W=MmٱG*/.ܴ=G17vDaBg@Cطf̜ aQ7o>q7ߖm5N;nM+D][_:cɧ_cKa^vՔ n qޟC] ށN|@};؝ q p~tLY4+ްpz;][DR]9.((c6F^Vǵ3.8}l<~[g@Ihpo#ibc ٣Oռgߌ4M`R5߃zyUH1Pj9:Bwf 0Ë:%Ko{$®=4jOGcР~w_;e1JjʡL>p?Iź| a/+C(lɈҘ*"ƋM;2#7W5>l.cEU$q|1BE{;>idY{!;o-;ÓAV!K3Fp4~lmKV_܂5%FkDќTd'pbcϭX]mv"شnA|RHbzGLX6xoQU>"Zؖ S['sӱcw6eԄK.>~}qƙ0tع[];s7ѨQCv5u` cսLl~0)T{hbf$Y5Acr i '49AmGp7Hf6jBY\ykCz9|do'ܳ.eQ?Ues`|[ŝ&Y1J>~oqdn|P rWPşJUf ԞZs;x'l΋,Z*3_=9;TWW',+^ŷN]KSajCѣD{UB˯80O?eK]+J-,:tQjVK[2ܜsBETVl͋?@%aǩn _vVX,RDϋ Sގ:d]QҾ{ Z>J UvX N 4+R tlx{#@=fO<@G^@w)TNe_$kD"閶تtzvUz9o9vbBzGzhc[M鮣BY\\^WYXHw>gm &ۉhі?{h%_ٵzG8AXvSJN ЍVpʀrr:-2zʪ#noq]RP)P |.Sj'!d]eu$obi7ܰPҟ)9' |tUU%Yz=]ϨsZᇟ)͗Ltᢥ)Ӭ?+z mGC3ٗ:w͚>&~zܣ>{7x3 ,8?q֯-?{f4SmƿۑN%%5w|-+Z˺z͗ Y $P ȀFy1g9I#  zPSG"qQɂYG\1rۚHV|emN_rCy E+>G>\#9J z$ ! 0{#E u[PO  {^z≟~"1u{&1D:D|2WGΐ#d #Ї%5A t칅=V#{A H$&QR&QGZX{.ճT $m i RAHy@gy~{9L}=QYFz'eޣ?H?O : 'mʢ=w4o=wQ!0EA3I}=,-?#9YÀ(YO?ۆP3 h,s|-+EϮH5iأ"dN^I0>*7>\"\@sXY'鞂?@<l͙Kرܬ8$.zaE[9~d/PRT>,#5~, =X/Qw?裄sΙ MX})!D2VwrРa|| ~Я~TұS;5 ., 9` q.jA=|&9Ceuy\=h@Ǻ}~=?6ڽX'j' D]qę ?upImIjŸ=Hxw"Y"/hK/oc7/w-ܹowX<.Z5;IJoH~eR݂A,]%?ӯ}YzSO=KΔ;9P2:Qo7YPX6>;IdazU3Ms}5 %}III%K"W\qW\h)!ӕb?<$~9jn'-ν5BɵQN Ⱥ~'x@n :  }sŸi7)sȆ6nOY}fdvou =몦Z#pkOTo[<T'oWux(Tzh>oRA? = _c? vL`'[8d\AC" ꄺj`D3}ڿar @U[7Ǜ[ckt><m {@ ½u9pN8 @fκ4Sr_ a =.=;H^z5MÇn,-ܮO>u~+^_ȶcpVu!QUKDz#J,._-К0DEζuHmrqW\qղ:ERc(՛D"(RH: CH`u 5 6@>:]t;rBMhn]gܦ.sn3G\pC?Hi dCwr\ u< ;}4>{rA'LKsGOIB/j :_Ps>(yᅗ~K z!c~?&~(0JH jhpPߑ;)07飠CuzxojCCU9K7}{Mxq]O8, Aê+'RiDwuC*LYihʻgL4wjNdH 5^#FLAE >Ǫ|#~$zl2  `uX H~ı57:"%5-8W\qW\قGX 'p ){ ߑ;1бH?sX="=G-cHmεS2{:¶'^}x@[n\(`@HA!PG ĀL¹K?>D%u֛hu uha''[ۙ@9Ћ- NOyJ}1H1˺u(#:3j;>nZN$z+;\{cBn|[Qof@4Tu&  RЇ!I, rzCAx?B.r@0Ȃ[A>t@p+8s@G @~r{qr rBDQlΪn-Hm@SLK"%4[=S\o xAuBj#G5=;׷!>ȟxE4kyn7yr==v}:YCC& OolGoC|q+8Sڐv#m*wqW\qŕ[1AЇI*d`b, # ǀU3~&PC!q䍀<#w.GExoO ?r 3.9zӽRsQw- ѹܞ+ d>vƏݻ?<-r# }Xp". 9`ΞQA BΤHIՐk| sOQѷi>S y?eC nqfڐ5+y!zz}"h.8QK.rc%zg̚}i@--IpՒyqW\qŕ #hӓ??κuHB Їo H[=޸~TM}͞y?)@gۼVo*wr(t'O9?ԟCNj+Q;} i:ζoppy?4؁,쁒eݩ[C"IBǀ[r;dzQ|Aԓ=yPooQ&><X'hwrʮ#zEf@gG߄? Gq51. ~!?IN2u@Ny۾ BȶyCy@p=K.su+8޳\pއۇp!{3 ǂ WA>N \.QC ήR" sH M:T^$+@:8n^z+@9R,c\v5ӿU9%zGsT#No~>urr=\00 Z|55ߵ-"0 3 |E+{$P;}6p@${\. Bc/[}cC A96 .'Ǒ ԁ㇣m{$ |,"=A4gRvo4f\phؚ9> 4c9'9DPSB lZ|J8OL,*я~ƛ >wϯ1X:]s k ]Ĭq\ϣGǘ\?-\mVSrdl_ 3yzaq= p/HmXԃ45:.W\qW\p!Gd!ݻڽd`;*%@;T }|^~[g(#{ yG!h\pcn3d?Tו0KVq[O8G7#;Gr nUHΑ , H>(*<PTv?\,(v-AL|^74.o  9=B!H8$e>`%QOs )(!~Ps|{PQ: @  j gh'5W7VX8kDmѺQd|64 RAH_)P+[¼@-p\D41M^x)}뭷0[}hcJj9?k!u9Lu~tޣ7{>3h+Z!~NM;,JTϭq@@%LÁz(4W\q=W Уu@/B#{ uq ~|m߰?.W-mw`֏H GN :D^ѻorbelU[לӋop@o\zCmxj><<!G8. A(pfR|^6Qt䖀A7p^@fVp\7pGuRA yVpVrH cp i$yjyZId)VrpPȺa@s, 3Uݭ:E Rz'"RASH=dE%]ٚϦi :}8ϩz6'ҿ4Z]3D.N/ vzSPJn> 94ǏRFŁI'pUz@qW\qmgkR1Οy@bȷCDk n(:\? t("v8k; 9"rs z@ D\|dojW,=#+N tc$PQB>CܼOjh tY6E 3y@>@ 9@ݱ{A7ji$Btf[lǀH>:*"#<͡c!1G[]@ { 9P7³}l Pΐcp,$ҐG~pu3ˇk HԜ(B 8~$w@ܺ{ 8|rDgTIjERҐ$u* u[nW,yN2 L~*=)=F-³xV8eOzH;t v Zo|mC|,5^Z*'.s IDj_,+kS?I} }$ YHr @!=T]wÌثsM* :,䱲Ǟy?n'ͥ<7S `GB8pǸD@*9ڿm-GGԝ-࢚C \л[<>>7n 9q |n-q9T r7 Rl7+;~Q!:$u   qԕm8!i?#WQ#r]$'ͨwjRAv+P@^z =~@+6T-r7@ocy@wu\/v_Ӆ} )Sf?5>k$jyY{|"j =4RcvݞEø+6f%:{@ l7}^ A-5U& A21u 0m$Zϭǂ8cGQQ6p {#Q! AB὆?@io?ZBxNvT^| ɷFD/<%NmHus8l]d?G9w 45FEޙ?6 C! < ~ ncP!JMdQH.3~l7׏D0Gj^jr+m 쑫vo!D ~\5 [Cq\@>)l*:+kICIE"j}wpW6ad׿ԽIf\PM\gTϳjշ6ɵ (5jbvqW[B>J;v}E?Ɔ@/JE/޶?~TB@sEŸ+ZZ9] ڼ$s-cھa/g6p˶zSn ~Y >eǸG?"fYsFfӾjVyĢٷvސC `^!G">F| ȶwq@ .jРurZ$wV|7 H pȴ pY@A" !7?rB]p'q7#j qX513Q^Y u>>!u@Ng9>|r \GմnvTu.6dЉ/Ɋ ñsi6j!"@[2G#a|l*uڬ<]M{ G|c^|Bu뗿M:|Hyh~S"jYsr< j46TI.'ɠI%EƸ+Z7Y>Aa'@Fr7[7:=?TN᠅.7<μ:}{tݠ r;~VKAN{ϲ ܦ.sn3ǔCZqceO@N׏f Z9Efj { '0 (蝘pd6Uwo Og;H+3G B ;9TW sA" c!@뷐H9};$5{HC wt u=.r;9@|"(w OhӸ^2b7e|΄]^FUExi56ytsׂ ws?.=WƋ\J8WXvo} cCTYt y&We"d- r,\я~p:F[|1+~ Ϟ,ix!r Bkfھəd?vo!jNVpșDIΒ0H>ѹ?vDA[ }ڎ.ӧdnu']3g( ~z,AM{@G]`W )#..ʊy>|I򷿽LdaB=s:;Ꞇ?. Ps \A:C.9@@?n"j  #d A60<ǞY'gm6{Dic\ !u6jƗ*vK@gJF3+U5}ˢJ#j.~}ـH@=V,=<1z M}w)Vr8uwAۤam8DSNkjk.cd1$Ww|ή= B. 8$S@7ڃ49I&W\q3==BupБ\z(, zpcrk6p;} [\}$ R5H sD[+9<'3X=lХË) `nD+7zRH"ε8[E95z (Ћ CQ@:Cky@At@ [#A<"dY; hZ npk8~C-$ LfP7&G|F@~pùHy?~OIڽ^ջUvwvUU )9vk> *  @ "?cu9zv3g]>Գ{Ԯ[ ½'|(!DvXTaja'@c@=@G?A {Ay@$xrLt Q|$=Q!\ѷwr*;tvw)5[nH>ȓe?IMU5ڿq rX@Î ?APTTT3 >($`g$O @.}#=l璜fgI]E#A`Bn>7R{ 򶁳s³G@ȫ\sIA_>]: Jtܦ+;V_MG~Lo5-XPk@дi}>/ǞHWA#(Ӽ 4  EKS,8VŜ+V-8ViW]u=9lHS,_ֵKo <??^WgnEiGaА!_MAVSwѢu'y;w4BK^@Ko˳~㟳[>>Mu%IXv/RW\q՜*H i#'  {wX@QuM"Iqqhj!wTIccA8 umOvp3&enֹvjqtm=aۿPGcAad!@$? <]}$hha/\g}-|I>WNO@2 Ԭ gUMu ZC1u;rrsFPxŧتk~j2:/ةjo@:5FZX O18xtqg_|n(3kfj Swz9뾶y]w BJn?ANg~ ocC=A"`@g;fB?Ap<&84~B 1 YA. 9 ǴCnrOX y?8s PQy@\8G H| AIs!0cCwޛČs~j+T ,>mV#. `&H yu!' ;"U3f\­|Y~7lx!7LO>Ȯ1c&~mmkf}z3cha:qb-{ v{kٸAsKNv!UUq;3 iMZK0iQ=.}~&Mب;H sv{eyɓ _߈9lq=^I_~U3Y?M#HJICH j//bW\qŕ- u}$[O{!𠦡b:VpأB?ȵXrr r5}SG"f?ȥާ%'Vvń{9fy?<+能nhA3Cs0SP57R+C3>}LޓYY d?29Ar@ ? tPnYDpHr pk8 r5sf]?,a?@?aCśDHO>aU|<G4gTfSk mKrAsԖ#h6dr|&AmC:)hg|13⊫u%^Lڎ@ґHG"!H*\?[zWՑ@ rz~l.5'b)cuM_7U7 nOX;.Wܶ:k3꺁E z`i ; y uԔ|s#Ay\ 150(|t<: wRw,r]A6Y׏jyv>,m7­t=d]>*u M6ND9ھ!'mi胺@Vp:yvomvêd?N-WE@p>k;~pjcjK#ژ4ƋRy"_A,*!u\\q׆ 7T RQsIIǐv! )'`Ӎ4AQB {Mcj @~d#⬮ ] !| Nj8l\m  s8$"k H0޶*0;p(YO?*GPԂÇ$ c ?^lՔp- Gvho' u)8rނu'R ,rB }}-50@B$ўM r#cN@$;GXjTr/Wt:|* *i6A55sKTT@HҗW\qIO)iՒ mٌৄWq@)|Tq p׺ܢ3 {|+;uAB!P5J^LYM\o zQG8B`.!VH;DpH$)$كh` 7c pY%uPܱ. A| zc)r!䡽>a"A|fAn(= $p! ʥYf Al=,;y=r@ ?$8FmUC>4gJԊ%?*Zf`/?C\ DՊųfW%** k# ^ ^\qW܃4f(j"ӑN_B40Hd[!AAqH]-d/g.ck>(G@ DΑЙ(G"N9d{dtɪ>eQMu}IGLZnI '}VpP\Xzl^szd/nIrb# @QQQM@.Cuz޺(= B{CTP@QMC|g4^G}F|n"ZH#IIFM&+3i,)# $tlIgoRP܏# u@Gt~G"Խ-Vp{fu A }f@ fF P:G= @ݛ!꺅A1VvyWf\>??9?P%ܺșvgR9 L.Y@QQQ՛+M3Hwp8}$@n^I#2g`̶3 HIiRwmެ,{)܋-pGj !8~m{r*_ۥavOv 3TIA  3n {uDi W6֢9!}y?&**?Fd(RW\qjp㐎#Ն$?wv-+97-ମjB+81 ~Pw<;ڼaO#5, ÇGd]@@ugPP>ȋg֯+ѣ6;~vyߋO. Guؚ?B,m=<48[A-ΰ#1G~$'!*pQQQ9m{8 mՙ@ 8PA" *ܟ0qݬ q ѹ;v8d@ J\v ?>yhyvQuU+1X:]{|.IMiqn"DJ *$Ԧ7|VeW`8o~'k|FEEԔ7MIH%ً+Zjp݈&iECFӁԇ]? xXI; r, ;8CT !@Hjn !, } ZB=9)(A/_Ů ' [ {Q@]=ɳ~,Q9r2pʞr$CH@} @ Fjꠖ[h Tku:fSSѵj`Ⱥ|5At} ~X?Ҏ?r;kTJC+Wޕ]+^VTaTT@M}r[hRQIg.ˬmV';ozˏ-A73Dp{ AK@do3Z-~$sr8} ! BA$WͶ{k{ҒC*Ng=C G"" |Ǟ@<1}faB u G8b: @ Tީgo=8#oN J/պ@].`j??z> @rܭސ0p *kfD3稡  |l;8( uq_>*USC gvCk6GLۼջO0ݰ@T !}G PAK8FT]yuixۓO>|9񹋊_ܓ4d+_!%.dcP3ڞ Dޣ? Ѯ~] |DvpHM ?"@>gA 8ZAݭߔ#_8s!~9'f:dY$Ҡk* :}Y9  =g]B;ԽP3h}w08)гɟwIn? ةyر%:$ooں]o~ޜ׾CX6rs:DG- R{wgvr!wD{&N $tm=CN@c,豒:5D ;,W\LG|3{meYTv:tڮ>k&w>  9@c֎T4TPfnZqo?B?M,*EEEԒa% "$%4t8,>{qf1i_R-)UJ<(jcq$ۿa/w,Q5@? (SK8csO@d!@2H {xl^34 ;nj25hhn)3 &Z;9־===5,CyA\*­2:D9أf0({ z= \6=n7<|D!:b?KO>37QQ}inE:dT5/8>{qΤ7Rc)wYE `vxg5${o7D+xA#)@@.#wQ9 { G8]:ᕝjj3]Ǘaur D\Ǻ'3Yὅ=D{]?M hם {8Z^+JڔvA{=&|~}a?kF~ru' z[ɸS\}?ʺ$K1& ͜uf:Iu 2mtͺ{$b/B ;9(Dw7$2Gjȱ|3@H%!@@r]ǹzp@fDy=5c뷮ءz7ԌVZcuu*έܑ\=y@3 ٫o wT u'@EEEEE=i)ɨ 3W\qm1#JJ65 uG^uBA;81KԎ@+5-\(r / Qgހ Po>'{ph!dAu G>rf_qa柹εSx Ǚb!3r:p ZP1v.5MG5|O훒U2xtK 8r ?l[V'$9S6+V>393Sg%_s(I@uzR?sAo6$G%C;9!z>VWaN@`>܃HE3ԑ(p=cu, O+8 {.}Xin@o.Hs9zg,ݒoڮَ_sD^G䙐ѝ]_ѥjH u~ \ @!3Fu6l7UB *Hm"hpԋTEJ2Dً+6 KICIHɦꦴu huHѺ \ g7l7p7 q3Vp oPӰǶ~q!*lߊGWf?Tח \0U}=GqfE% wrp`pmYd[qRG"\A$\K;9?Û cCM@Ef@V,]zS~w{y:)+|ޫkytK|>G&;209} mSO+8A칑'n\? ; xT;8@`P?`@<sgm@rwHҳL=ٞfsi&mĎ! {Xs :G5d| UW(****M2ҡ$iL!W\_}@7U5w|Ճ  {9s5'A9#u9t/i珉nr]Ps =*_g .ū wbeI:|9gW/qAv`Ϲ j;?V!fH G8@{ :έ#|۞ɾL:i@;M=d̘I,vd 1F_!3|d¹Uc OpW@V4c>gՠA>ŅyCz s - ܃GrHq vPs =(<'|~{dƏC3_c&C' t Z#q.sHuxAMMUױr*eɸMը^5; !!'rh珊pcs\G3 ʊFUG&RQSI=3W\qm}0ǓU/=oOPm,A{`Q/ Q!ZjrHљ݈HZ *@5ңIe 4i8cx2Co ^|@^8]w݋yjŊ;O2sm7J5 -_h5(:r]>p?l7cqph0$5= 0A. ~\G׏ʝGG^sai׏̙̓3\}~j3Xku@~>훳@V|@;UAu1 P6e{7@&N9矟Mkc n+WII+82Y;"܃ Y=Z_Ē:r}c@vp#pGr`H;ocr"qs*v;?Nu鑋(N~:~$.쑋?u~< }CmgA>s Ur<\@-nnfUt\ʍ7V4}g9 /~>F4U_lUCێ;(3`3 ̓L*(@wC7SPDԈ8ሢh *58f2&1&q6~hŐgOuuκnP]k{Ϲ^WjsC)8ywy7粒+tٶ{?R_-}YgX>MGDOoltvܱu8-_|4M#?樓 Uθ x{`9Xsƚ1˿yz5(ܡKwop |YЇ lAP ALCXdЇ"9 } ?a\p90n+=oFEEEE5ST%Jr0ѷՋ#86ZfGW5gtӁAo>!trDJ)`D޷N' aZõx5ZѳerZ0X&΁zGQ}O>9+5aPQ:}X9aCe\k5f nX ͘1/W?X,L|nrѧ }N!C@ヌg=1}7ަ:}\\8?N'y S9r|"z7; E./`ney/RǍ7 }d՝/h`/Q̝谂NVe%mH?D,9oZ~nqAG#:m>كzMzy9'xS+o.~q!X ~|Ft0;cud?;XClPQqn-4!B[8(ǂ<L| p"b J; 1s3Uҵj)6!'#ᆵ>kkI>2kC0߫HAVn c!Q* J+=eiQQQQ} 扒W/8T!(:;B}\9W 0n P`blc' }sh/|1A! Ɇ;3EdJӅw+jrsOjql:k( iĘ9V9OCA:>@\s_ }(U`H9t9̢pl9k KvoP[-@[\Hr.sm [wރ>qzgSY9_&8{2˳>oP^#h,d˷Y >UzO>wO%=9/ ScsI>wq9(W` '/OExn컼\Kx jL2pfm?cb+ {Zrnm@o<г֠G{E\* z2ot1{^@P?VBoٷVl[f63ƵU@@~l8KYbG8*5֘uQ,fX|Ú'!O%95]!^8ccVtR;u%mv}}!Q}9o 9ㇱUٹǩ; ( %w\=srfMkyAP9EC^!P~\~Da<f'ƀ88:~dMgb:c`*ס1@螺uzuNCCqν0!`QC y!AG ljC$ @HGnCSή/jlx#,+K:5(贩pՂ#89㌪d\"幩sTr0aSQ1oݷA4^z57ܰCΩE$qO9t9s.{y 䒫.GCI1x1A(׀(ŸY5#bLĵ=Cݚh[?F?>c3Dcǖ#! 9|KApP1a *ǵ}xq1;ZUeYY53N-ݫr@&Y k(#Ck]':,sG!;Asvp''KEEEEE LQS'W.8h6*aΆIAE1W!bN9|`B' a5dUy8?llK{Q y&B. SWɹVl}#O[θ{ @AO2p> 3p= D6*A V5 q@'*:qXٞ:^x<|=9H6yʌTSOJ̽0kZn]?i#\Ys@ā:y,D ,Ђ[N-,|T^J!ϲs5܄=S{ %y{߻;cρ z-_~ VqÆ >V[Ev!G@ dgB!p<ž>@t8 1gO䛒(****oн~jQCEEeGMӬoVe97@;<=Xۘye a0Ǜe\Ĝ h !uX@;`~Ͼgɱr`^ sK7ܕϏq:ؘ׏!t`9 y:[P/ sX7-Gأ>  ̄)aNBsM hڧ'w#^qiO!=?S lgyず .J4Y@TÓǾ/ˣO˵Jˣ:NO% {ΘNNsGO?ԲH=נG8g[v̾J_|. z)wU7NC ,rG@ٷ6e̱ 5!B;{ʾQg Gà#yΐ'Ø б. ;Cㄜv N)96W-x-. ?p>ab:I% ! 11Rp\@QQQQ}ÿdLQ/Qx#iѴkAKbOY ׀5&ΟpAe( z3{AĘdzt7 9΋Ň'7-})-^חXs R{YPhg#H;{טិ[1t+O@Dȃ">K d׌e;rc UGҞH+z̷wk+BЫ襗~}^>+Gpy#.:7:8VIYdzd8Oŵ(='|*gΓj˭y3{̞sʋk Y^>i`D#bvwƾZ16ΔžaPʿ- ᚥ8GA#*ǙnCxyT?y] {Azs< txfIthcc ~tP',( Cõd?9(:"#ﺝh(ɩE+Gq4XhNT֔ ^2٥@R/%lalr8ͫ5Ї";sv? kk5vgw~٭\>89r&% =@A D׏R-\@<?. TGYõA< XdLlA@rF@52iY@rCF@k֬E?9a>>}F$~?Wk~0MFqۦlyz…z,YrY|ȑCJt8r+6}砾F{gޤ禹6m{ '"{hc ,icVm=8}( a,?A] q2ph9?A= 46bϘ (s?-GVoS݌晛GJTУO ǻve8?ee' JAĜ3DY ͋PfH¹:;z͙k< ȂuN k[P1fS1As}{r^uɠE5'Nq,?r\[&speA ~ֹG9ҘЇCBUms0fj@h!8Ck@OK !bF7h Bn{Ϥ_ΗcEtdo!/qdk8tyjȐ'RpEJ!wuѧ%xM蘒=H zn݆<5w"]y B@=Va,p0 석>apVb`'vLL  rJA7+)ü-=`0:$:}2G'ޢl~r(ݳs t(<ףc{r֏W%9yͭ=j]C@ `u (? q_- 1&Bo{`Z,!r5>ܬԡqJQ{,Ԡ'#vݷwI8q@p븲ipRn>$}O~|kAs2t'!__4oEV%|1NKB .J>븾<8CM= {wr4?v=?mj%=Ѥ(ǖ^@t6pyrQKHp ALĒooV wNJ?Ti7ON" {a-W{e6-'PԯRo)B }Rqٵ>($|T9`73r AD m ;6߯{~=W_g+l1{d8fS\ ;0txc;Β/vAG@qlh-:RTyzqGEE|y=ot0Sb^A alqp8AT0sV7[nY2MeUnpk͔'~T7*|9B {)uNm ]3Ї9V%\}yOq8ҭIzeX&y7Q?_O{{dS]>:@xmDXKa{AhĈO>Ǿ-?d\+p)qfWXqGf76WzMV*hdaOM( gLy@A7PA@nO3M72C~?ϦTn >{pGsWU-Qqc K!Py]k>kɧ?³>_X++OsOz:VAAsgs?AF/d/qKijm_`$86;&|±;+Gq`H6eWyY]6r9?"'B9[C0fbN c aŜnyՃvw]Ռև^v0\C@] "v Wp3% 0o@g9(wBΠ%( ~ZZYB=!]M#AcP7P j{4~+=Rb>@LE.O@r}70z L/H<+\sS+yI,^װ'O+heˮO&HQxNp"7:>+] ;C0ȪJoE$k=zB*9,{p{C^Ǖ|.UwFA)6ɣԞܐK=n z}g}c>HznI)7Mzdxbu)X%#Hᱍ ?meރ{~ B\NmMN;~ڪaݧ@ <=-خmN8>@?>nέWoWߌ,T'aoĐ;8Ͼv=' ȭqؖ@yD֌V?,Pyrk+J#HTg璂Ww>BQ!x3B1cg<^>X<\e[+WYe+ ȗ"+6uu.$};^8fZ4D4'•v>IVǔ˯{ Hf ю 1k]?=~`&Qeߌ'',+vRy^u;5#~8+f%, YBTRryۇ9T8'ۇ9;CXę9 }p4@?e}pB2q?6.zg7fɺu>Gt-(KV%$P1p`O5/%@6(d^Ӧl*cO@5e0y~p_DXC8uk w(sX}.f:!DtPt0V'PsF_ ̢4SUYsZM>7SK:UzQ?FtL0~ Ɓo  3!c.5sļu>@8~owV^C >cz~m$|Q7IH,S`P~p^;_X|I|*L$_(ȗ2.z )m[gqW_K'r=Öe߼|1~ق \ #`]cu|IϊrKDК f~BՋ#o?]DFVYm?[?x=|2pn'"聐#Q \rp ^#eDvwb[z> bgCzcBpmJaB Tի/*f>,gE$9Ƅγ/Pj\}k}}80pLHG?aac_܀owH-Ln6_r>1eƐ$7k*oa,#Ɂ3 [ >܋[oSQ g9 wA-@<Ԯdƌy/#pk3B' 8|\3r+Yk+)'ᢑkW\c=f ӊm$GR:\[Y.{ mqrʍ;wVc Ɓ?֦r/ܺB׏q!}R=~3ច=vMSRoN;@y'A2k9g7>^~8$kǯA?eeT=sߣɳ7W'}llK!(B˼q_r!w`05 1֐zLf @S氠ç /6cBY.@U=|iqH%j|Z/oӹ _~@ҷzugJ_~eA>3 /M Rz]dY@8"@zGyOh@q4}-EDIN-\q|3h/:%“U9%.<}lo6Oy=Q<{g~l9x P7˵uU3rOoyJIBL=0geAYq DqO @elWyq@kx X09 !h!PAm7juJڟ/ld!f[|\.s:dur&TRY9H8B樀[kc4AO:- Dcp=:!_7O. qM1GgHQ %X-(U 20kp\ҩj/qLjq n~t)+]-|l`N暪1~?@ĵ z<`'( {&oÎa j7 /+pX^7pȹ1g`8p%ѕ!p/t64/Boqѧٟ~/_$ '_*XK(^@VxȜ kJr|]%_8U=$_J@yw.JrjmrqFG`5{Tu0"9[8^=?=}T98aU{seLC/."B  b u(\Ɛ)W_f(֑q~@<sv" "p 0GxAAzsڼ3ou6i77#3}O@ʖ} #(*c yȸC rp=X[q_ThQ=n6Qm@DXr~W_ Y#Ǟ:»Wx?JaOnJK)43}g)s PX2hJ} RNq7gxzv-O)H_CU欘.^k ޘ+5k7F/=dV6$nr?x]=pI)Zo|] )z_/),~\ËGX|[zf/:TYzqGh&Bovr s8 X  s0oJ0smʼ O[{vI/qfѽC"~,yc',8o oye*C-|=C)׏vxJq@ ^(D參=M],1&C $7F518hlDӎy[Mr ~[sܷ@G~GlA)O~{@9v$ϵv؜oP~G?1dPq>=>?)\3rdͶU,Y^ %ݫv}~`]T?{Wqf*TM R#8} !vBw58O6^Q(=JXakH'8Ux*>BϝCj4ߔ/iDo{@<,ezq5s ) ~gA;+g;RR7AW~ߑ ۃg+v]{Nza6_IaK#_s7GǦpL4$ED-Ջ#ޢ"F}i7;]s6ߐ \` |\bp?oսdTTkNlq֍}~$oNK<ļu A~Cq5!BP؉1^YCub vB 1@!B x(!}*O"L˿sΜx][@Mx= uV[# HDØBL">6.|8|Ο0džAt@,Avs 0r(wqn~œ-vXeoJY3J vaON!%9 Ug='( "!ڔzÚR)sQ[ YoDGn$Ν2,H/<7mGS,eɧg]H)ȹ="<6UkJ97Jo5&1#.:2_![OvyJz!][暛j(:ԞB:[̽ގ>+{9O=w,W=IE,nx.騮Iz#8yQNPzsga;qC: Ȑ|5o&)o\&x<(dٲAFkҸ }\p7O/ 2puXcc \0(@ w8sͲpx@v?ZBg=jMbjd?P 93g49lNS?qP3ΐوg`bq@U 8kc&Qk=5vPsMq1/h36q~@p|K!ڭk:S+{(իT%܊)fҫݵz zȀ 7P֏{.a#pC&0+opy:tZ^!huq]:)=@S |S@KJ$98ptQSE[+Gvhh~QVc gY8'2ACy?CR"A~ f" s3n_ǜIٶ3[xf 8~ЇCz9 3{( D؃>=AP&'#2{uB\S } 1 (Š mb:CID1 x6@V@0".n~?<1q)g>>Pƾ.F)7O "1 +O_ ξr![7 {E_zo1ʕ y8 jӶGgF0H܀7+OC9<7$k?xP寿VT`PԎ=@p68ık' dժp yz* A ;Y1sÞ} ,yd׶c"Qsk nك8pysr睷ľ3t1kxRC W"[U(:DT^qlz;fF>>gj3n?!c'B pVb_% lٷsoYc[r~t^ii6i8UMĜ Ğ?6BКl0 )Ocg(|r { ,Zr. ƌ5#&\^g98 Ӂk hӔ6I 3$=@a悮 Q ْo@X\>PdA!G޺}晝SK:W C<.o>e{{aB ( yT*g=\CeĶ?<p0{"~SSF@׍,*3Bo7Ph(8tTyĭ\(CG^zY r%|1{7nR@'A$^n/oqXIs[iK _Is8BnEx/0&!0;zͷ3us ʯE-:j3(gزm(g]APN"jrKGb/h!TKˡL{ 1w݅F(rX2%.;%95VErq4bD5ʷuNSb=g "sGkqΊ~e{=9kߦdE-S@pp|@u?A>]=!} <ȗ#o}{>`/D^+X: 4vHqcO9H94u̧I3<}$ /=#{?PD"B?,7QajW`ORiNJ!'n,F$o 'l з}Rp#傓=.j I ű)cn-:@4Gd W/8s/QeQQ߅2~  0k)Qz^8|99o*/߼fe/:~]@^=Cı. Au?'E?JY{X@f:1a}'zLb?{EEE5jg?~-=O>8;C63Aٲ ([^SN \v9-%X-$C  akoN\0[nOay}{u*FCs S@("a>al?Prp=+]*ލM=%6i8$\ ))}=M x}0n @;ʰ!+)/hT3p&{^J?k !y |#=7kf PЄ !we"6BAɡty{Iߟ4/*W1wUY)86%95C%^8h?TD5J%VADcc>ӽKGw9(oAdc:~8h^OXTGO8N Eȃ9c1o\?"&8x=rA g0h*=^胵=#fW?wc3d{2p8rQ![tzonӫ[~?e_|>? ˮ HbKپ4K@_!D<Ɩg$LY8NO7pI/ cS =JzVO.7@=u %J]y "xyGQS+G 8$‰Rid*@ODc 2?&r\sFnvU!8@ }lL8 zl Ϙ' }\̾?א$AP6"ۼ)gA!3DC? HǶ[f1r PTTT]P;@VFi7̈'ǘ}DBt ^@=?VaCc;C*&sF7&Xg ~xQk77?qVe;V \Ѷqz^UӇrpg˾8 u aac( gpM(c|N a e  X k xX r]u^s`Uiz>P@|~̖"c"4n>>O(quSaRnLwv'  ܡlL˾!0(&챠1'܁~k*w-ktqx?=qb !. gQ 7q 󇮠TAa@=8+%Lr@Rp8{Aΐ^<D uc <3K@0a"$nOA#!!qovm? O -L??6Oõsd>0Ywg:~$J]JMmNYa-F B&Bdb >t?&Ѡ"б렂}WeC ŒH, k m{T@۝?C{(7?Sp^,! ?N%DR\xݺu6>|P[x@j͚(W蝔q<@[x?}q*W0 8qz8#=é#{xʹ= 0Ɲ7A,c$Hbp. pʂ-6rxDpmYא#ݿ>g*ݗJt(ɩD{+GuC;5g2@9VRn}~vky, 8"?\s_Qk. AL{Xs6k2q[`M! :يAȭE-B1pN,~@EEE@]')S 8g~8sz)IciO >tq`Nȸ u2(G8؃NVCۗY^gvc[ } ~D\ =(y;zΟ1"A8}g.BA㾊 JKQõ_t5\f Cy£>Yxͷ]5(m~ Ϻ'7 :v>@I*@)ܸF/[ *wy>7őg&ЇG J L}9X=Ӆ+9?.',kW̆תHp B"Xt [WtL6S|}P[=cޫy/#{SR{,&b6\ju)gnuދ+B8=xmx-` &%r߅A%0H%E{$~={=ޟM6 ٚ j\qz#6ZlX 0k'ZzaJY|]g۹ԦKFqx]?ZC7 qߗi[EO?8}JYXL8N y@y}/a߾7N=|ɭꟑhړlgMC3wȜ;gNBo5Ծ3.pѠV~hzk/.Gheh_ - nyc_0s[бܴ {c`~0<G׼ 3#n P+!'p jU CV!z>B-7P[Դ!dzYnU|z"jA>w#FЇyCDsx<=zs{~=) >^k q+;m|lϿG{t7 ݷl@gs4j!Qݟ'q긨e[ )BAM*zέhE w"`GaO77N]};<x]Ch]Pw_w`t[p׾o?`D~C/\>&d\7AM#o.^vq:A1'?Bz&'@H`ڀpCT)! FԸ~,qOPx,We OԴ.,R) 7+b乮KtF^Y }|Fz'ܩkP2'P.>>#:yӡ7\;?.{ّ;~=LBP ~z Pޫ\?D\>'s'A(r~<M>f7A>ON}'/N^?sW|m'?}ɭ4Gu׿VoOMM ASmS_(rk|o~3 J4Nmhy=b!N?/sÏ3|jn~p|ȵ&gRSN׽EnzD>& 0(=z(@́@D>|C. [cܴ7po~4q,N&r-֫PK+\j7DfCɉnDO/_o?B! A^ a,:q<C9u :~.x.}YQG:} I/wu "HK'g?,OϽ]ӸyPKExQv| Pg1BHn>|%H b՛p}r@9Nw 'nk7w_ߓW]455Ծ]]thS-~o4sOh:zCV@(. OcX;o[ }فЇyPx7=!!>pm x*D4Ek߲>1QOw4t\cyJT#59*}ڡZV!q^Cdyty芵zy5w-2#/ZGsZLqȷ/[˗uYJZ-sF8t5D7u吇{׉ˇZ{jj1O<_}O>lj_V!;=\뵯-l P4|c&v P9coGV! >@ 5b~XF:A M\A'uo?%= > 130Hrj A)U{ A. g1?hc `OS6=$ r)q 'S;D0T z"0(@'?phqV+q4]`H {PHާRC/jbQ$H?@ z$@~sӦOL=+;-kfM}?":"`NݣOt9}? ~'= 8f?xMbyá; HCN-k{o;}?O˿Ǔw;555Ծ):E:XtcѦ@剫ӫGhz%5:9xdžvT T;7m}:xˏ<0y+=? H^Sr.rAѭ@\@BGJn@!@w&Q }T]"Ǒ9Ik9]-z~'zEoE }:3K 8$*YP'buS{G<"A'u%rsH E`8}BGp:But[? ; r}Eok<]Gs Cl=v:Arts]z.۪8P }|F- Y K@IP>߼IA zz2'SDoX. )3< }5Ǐ?{r0xϟ>yrf-ijj}A{>[4.n\roykz9{t_\\vcZ@(?{@K|ޅGz{}-w. $`zIBG?ԪU(\=E>Sk $h͛ԩ -WT$J?&jcD#8Dzruo3 "^ }! X5oß_;|CWnygil{>2Y}JTOv){@=rɣo+)bSl \ ?!"C wx>j+j -zhS,:r;z_|v2֫/9iB>&?HH>zgzv-z AA bs :DODO9}>Z惡DCN )#M$9q9GYG~([ '.=] P!"TGG\@Iku(QKpH[:$GsjSrr (YLϋAi?hx ~ c5J־QN bf 'P7pGaP%';?bۚz{(we0(kr\>p0WSnlƧ~ny…? yf9~gQAO[7ҨjASS}:}Pz{\.VRC.$[@ 55555Ծ3m-zGёA[ۯ׾mʼn+Ws@ z?R w{>\&@('RDq$`9";VKΜ}f\>#3$~ M VYM,o "s-\ \Գˇw3?^qYEw$vrXMtMP}& ?(?M׺{ ױn b0>e27GM[¯v[zh})GWt(TV=+uQ}&;@#ሙG1S%~};;Rb z>;;YfhDy aBz|Z>y?* SCou^x"u=CpH.wdM'\c@:~rI>k@G?FP}3ˑ:~C bշ~;= 1E`Ej{}M־Nb'>+1B 较==b@z35 '7{(ϩɁ>B]Q}C])pͻoxcЇ6G j7jY..\Ѡ>[r;ȵZ+xWA zvpJN5N,'&=6y{ӺaC`OX Os׏5$+ނp*8`Pa+t E{cF-nrb'?=?3*_VGQ9#:'r7 @&r돼Лw~+ރtg $PHvXWjrO HAaWǏP(vBADqxHYiwBME7o~s4rvk#_88yjy`|[AOɹTrS{'PYv[Y;'掌{@8 =ބk޴־:G- ~zZF;RMGz7\ ;SyZӯ@O{OπCX`?DmfoY{z}C!?=չn@kLHn"g-\|:@>{ȉN-^|"ޏ y_=r? }re9򄮠+Xg XL}+X&=u NfI?y ?9!Scv= V!@G5PGAz<B=\> ||ǔ!@3\>w,\:>9{oO<>3]j3z'_I)A|tpEA!p&q!>VYɥϪ7"L i+j_hhCEo)~M_9; 94`ԑ[͓GM\Ǣ $,znz/oySwV4Oo:Ƿw9}c{@&oT!F7jGO$O[@H}O kd1O%+s\eq{91ѳh=f=3>J]@r BVkPrG"anoE !.;. z"g1\ǜς 9Ctx3p)aνDnb=\{y>3z< 1֨Gy- HXtr#+t9=!Ѥ.ɁAD Bsy6c?^D[o=| Qs@D1egrg2Ըy97pw;Vu3Gߨ&'7Dkjjjj};&>Z4EW_}g9BSO60r`H;[WVJg˖ s-*䰇wz: _o7GfPퟪ5(`@j#o2A!>7t׬CIoT̨; obçsO )>uθ nˤ !@yN""#7 FIrcd>zg5CrHy!2P }dL9 (fJT  !?ԏ$ }&Az qѷ #@!ؑup A*yp HAO긨%5q㍂^p=5G ? ~(Y}qekxǪ7Q8"]Fwsfz)`GbHN 1u3&ˉ!/Gћ??5o/#ЇG?}'g]fsj#sأzѫo*@@?إo;2:8C@&>~ȁA'bƚ8`P'^;!?׿!`jiYf9ֈm V- zVfܧjjjjj}34\t[LV~hz{/ ^1O}{mM,Nq*}'SguXn92oĽ;)=Cx)c@sqqY x'}`8}ʌ78zrJ)I`Pox~`QRǏ"fM~p@ (Y*33UNLzB)ш{@zQ ־Q[Cu we8Zი!ݟJ fZ׿Q+BA z'[FJֽ vQP ȕB q x zT BfVY5rYG 쩡8{<;ܱ91s8(M'W~ Dc }|]؞>2c9=OKtD,ꁨ;@8Tz'@N?:S7 d .UO~PSSSS@k_)M<:T>R4x5s.|?v|gdh=[g[,# O>L<.)=߸k |nk_ޮ:z;?|k򍺨&q7dy{ȁ:p!np̀?"=CD@$ѻ@z:I\? Ny1_}"n.ؓ弍 :虘z"vxKD~,}Yz}>귍C Vq(rQyh;.`x_]`NC* H*+PG@(?y9G\?2C tub[" jCm u<z`PSSSS@k_>U4詇_}'7`̉T^WuO@!@GqZ=rP޶mǾ .<{k))?HAk߀Z#8E x<\?֓}>x~*HГX] ] EHFEVU9rbG/A9#9ndÇCޏW%ycQC @yGD]0ȣ(s#Dp9f^D 1 >ty|q @ y<>h yzYqpS")!L P7A֍@ CCp~A 8DLd'Az%jjjjj}A'uOю˵53sOm^Zjp5kF %@[fr 8qh6d2+ ,ȁα ~;h%4_Y}L3 3z~#":6K G{ x\ލ3VL@5IІ P-ÜZ :,G}:#Bߢ? ,֕o'wF{}:Za\RUo!?^+ "b!SWZ|y+9LjxYh#=Ɂ?zGy)-( VZ}za9==7!^7!M = B7~g^5sdnZ11$5oA!nҍR}r b.@dc=r\ԡ?hoQY9N9j_hhCGU4~olзݞ۶槿y4s;}cm|-;?#s):vzLR qri 7SɗG4٣=/pM?u XD#2AQ珿#n ﹳpQW"W"ϥ+ހBD- ވk<"@:6|H3qwArz |ON/ur}8uO)ةz;X.q :>mN珉@DSj;0(]ÇAn,D[?@@U ?V. {[Ч9ܱ" :}L"="AH@yp[ nx$_ ۈ'A@VصnC׎O>ᱽ߾+>@q?G5`9Q*0!'"qHL}.n#HW C(y}ov=Ή{K\Ǚuj,'" *tjA h{o zj膢Mkk+gGMEwj3.WYkAX9#{tq@1 9rɜ?L6/+B{;.?uLc)Z(uGHaј8Vyɡ}>w<𡖾 %"\־Qc3" S@|#\@U-C2\?qATp! 9ޔUb&>o .7 @PV3 y?9&|Gp{<2sQ pjω!O?;>k9=7 8?[\7>9{o1glom~ݾ.O@(py?>ַ!B)9OF -)u n&`OMkQ }Ah?jX!?"UxgGljC>6b5OS[Dݺl[MޣGlY#[Pfq}kߙ.,b{Q(~2x ~v7[ǮZI/tN|l> /?>U*7L|[%gYP>;@rAvw$>4C9~XWG>t,"f H@zj=#.݃!Z'zԈi.@ p@"rg"ASu{(@ݮ\}<79uoDQך A NGρ=5) r'w Hs?US8~d\%R,Uo:>Ժ 7ѵo . w*:׵o&n1@Z+ RgPϵo+u!HV9Ioݽ sVNuncaJtcZ?H@z x+:?hX޵:~ȑ7-$'+X 2hs7cѝ=L@ X)u }wy^fAi(5Gk\@O~zksQ׾}gThhSw~NhзGC-쿘s'gW]?L\m ʜ&Z43z~䬂cEӦ3:lsK#o}f?D$.>>0pXA@+^o|>H[Kyx:kj?#`2+7PD'<"z(r $GV,'^V!"5Gbp PSd3O1{xG/*<>zN)zE?tPU @ h$0Wo!8~ӚRgu"u=o 5=u(n M<69vwqyNGUM=G %dݛ(79n H#=eGE9 %+rBn Ga }ޖZ>&N%Cգu2=J4wBCN!6gPAk_׾3jHT+E[گ׾Wj'5"w{_\ AW ?84c. @}+*ڴr~ܝ?[wr:>i9#']G}eyM-> )51ZG, 񃼮gwQM{j.e|B ؃h-w珂  (dRXR ~(ph{rDGkE~28TDmuYrucyt0<nj<5aQg3ru+!OY#7z;D?DD. ^'Bպ7qX$Ny"𓻀.qysu'Zo9jqN="?nvcO49~πP0* ]5Gjsyµ}k_tAч=X/׾W+nn(z"tA` vO M{js̀O?|s5p~,<42[lٵPNuoc.=}@ uH1Y bC u!?qYC?~$N ' ^֕>=TjC:87r/. ϬH3@GC.u~{>Qz,x>EHP.`~a'h-NEP~B!7j<]\XAydUpD!zGzDDOyb%W>59`(D\h.QQ Cns<*D|-Ntz 295=x" r 4rf/,]x=<:@B X9)frG Pp QY&nb^Hboz9PpY-0HGk"C'Q|*~| >Ԁ b.="?V?k }k_wK>_4ʹ_}g>ԵιKB] 7y͟jN&?wyg=>䫫,wc*J8_[ϺɃ+>_P}=*qYNΏBD5}`AU2' ڈ kߐ(}upݎyyWCA\?>=>V7NUr H$@VoDԿԵEAE׿BwqH 䡮@o:Wr#w=OsKaw&}Uf A>Mt#(Â@9) O7  kpǢAr3j@Z+<HkngNqI?vyGO7Wnyʷ>?^[N=$=q}YzD:P>@G\Ç<>yr\? hoƬZrc̣`[XA6$2(wKzbD :@Q}k_Τ@7 zEگ׾8~.)P?&Uo}VuOn,.D:`3v3?]|Nd7rOYVrgdu>7ЧQQ }|=oUS1pC,,z2O֯@?!suo(Z8PBD9:> D(}[!yS8!􍮀 pǤ@WO&+Br 7 ̸ # y>Ho9JA@  s}@8{ s]G ~] zz ˑ@rU7+=?^{?ChG{DaFWE[|U }NLB}y'rx$C?rxg֫(pzb"ټ Rd uo&@x|Jl[: ~ȉ3SQo"V bQun׾}g+zOrѠ(~uLf]V&׉@Xnt? .wT@}/rUD 1JkvhzZ.#S~=~G>YWԝn`E .d wr|#6uf{ˆ<Ґ"!7Pj. 0(XmQE 5` C\? lnP}y2@zǁc!uҺL:a}Up 9LO~\"c\@ C41:, cs=7*97L{!g\k5XFpI ]@=pbeoYFѫozGQq>xv\9@T;?ޢ? He.  o@J8C >C]9(\fu% A |=jM>ok׾}g (XѠ)rEscśGԹnpML\䴁k Org|` }\?p :8wq@&sm-kYn ]_{)O TrR{qR7jQгM= 6wo< uA@" 8}LP}TsjuXߡETj]f=.!Aw0Gj tD8\@ Xm f%]rP|2cֽ!`Q8D]|ޏ՞W@=?\]?[@3HB*{ֿ) 1Sy$3<9A3I xM }C 98LLz&uwwPwph1CqFQWi8 D. ?Sأ pk_׾3m.׽ zhEc;Ϧ(E nh:gt`e.^S֯_~O~cy`;J Yn[?/;sОC@>&R /5}>3I Ojq@{t;JN$wIB!?!a5^"O݈wcE3"wb%S*ڔ!@|}jh=SD W5\ X_VEuP4rWP[ ׾};@9E+ԡ[ڿqŸ[:PY]s͞3sdqkv\Rxq) 看bn|X. Tj*8w|drn??ֽn}*Tb'ZrYNfxDu[@=>CqYA ]F$>]@Hu*} ::O-QS["};V㹉\@oq>_!X%pg@bLW(wLuS-H.>"q= "oD )"O@nxA>^A].]gQ| 0HT!}8vQCm ; &7}dYeB/31Z 8{QGDapYx QB8M5+rУZ|;@&:}pwȣ7}pHj"#9uxgEvCHg8}oV#G uQX]?y"jk׾}g*)tѠ>_tiΆ;Ӷmt[~rw`|FaOZ7`3)PKR"@EJ| 9<͟5=*?3_|M MkP tr=Ga@|fsd5'T 9PAQD+d5(GzG׻~@=6"W*N!?uɋ BC? {U {?1v~DoD+?W9`HvE^~"O Dߪ蔚$p0?!kэ2#2[ A@-Z ;= bK!uwB'>\]?}$Gu%zkoH^G-\?7|7ԱfE.!ةA#2wC!d (d5] PƢhO?XQ׼Y? #0H{>s@} B *Oأ+rD ֽ3 3Njz}0Y Z}kߙhhCG~hzgTȓ.w;km?@=7oOZ<رu~ ٶ5yN3$N @8 NF'nx؎ٕ-ij%>û-XVtsCq:Nj!~5-ukQ#8K |LRpü<窷<|0.f{L ##\=] ψcd[ "wTz[? 7w|Y4J5Ծ};@S4詹}7fs>VtZ??Yq:tdCkvv`t@PN?_w~y 7р2YЧ~V{s ֽst~ 2cgr ;}z? v4{=.>0pЧz!!5.dֽoޏB!=&W!hśs#5+õo|pX}7P`'ykCCx f{J8o}ި?zL8{TrC|pP4;^q0  Г"pӈM@ :rWgCnˇwf:8?g7x[6?"#97b0s[:?rlx[ A2C 8#0(}CH\>mO{#v\ |RȃA~Z}k .*zhS]~/z+`ƍ;To BswONZ7p}jmKfns}bA^X?hǣWfN w-nKB#qV7~T@Y9 w ?{5 YFDKW|#wr{@t>{?AsPϕoCR LpuMn`#Oab AP/e U1U~'=u[cI]>.={93@y>VG1[]FEY xh4 cQj9r$ $@7 ڥ5G|-f0\>3QOVGW?)ȁ jIr|B^z<҃OL=w={Fp@1cu'!1DW!@7Py.gH[@Du~:nv@&*!7U9pd9pb9=Tjc5`aWOyKM?7QsY'sY/GkbODzj_׾AhS_ ~6+28 GOb8BSW<;Qhp^SiA?L\m YmjrgK&w**r>~ a4[]v׽;#ÓGW1c h6OJn@Є;BL=@d0Cyx(^W9,׻?8u(٨GgD(D]@9h7 uq H -{DS"v@@ "!3`JAgbnObv'pMWdQz2  j8b"uH_@&>3C\>J8RWO8רR- @DKּ'@DxʄgZC)F@@;?=AP_Bhd'f~vtC9>Uav(sH78{@ @EkY\WY:Dz&q( Rwި sy5@Y4w}>p'zcwSG}O,`9Uf?@{(p[ ׾};@H;ϠЖtmTwuW-fw(nܼ6[`˾OKp+ X>=xY7F \æ#!}!@%^GyIA2Q}]!@P&@UpusP #.HSHz9>kTd! n>\!`}u}@Dr͟ qC #ux>uЇ-ix@LOrSb y!u~&jzz9V_tQ{)qCUpYmFa:AS\q3 ??Dfh fJppޓ?}\V7%7;KO}`vyl{e x#(rGr:8 G?(*7}Cq}&';q;d3`H xm+7fE =*@HV$'Y;~|}@ GEcG=>A>̻@HO? P= 4 >pj_׾) hG@h/?hpus毴H5sO1K;/nwh_/§ {7˺o}] i Y~W N"n!?_P?c4>qx֩-Ӄh| )qg_(ri9u3 qx?}Pֿy>\@L9 sw~#F8dM C_A::wFCpo==m!xqHΜp-rEsY:Y4UGʉϵS; a9A*H׽QzFl H7صV.sWN/3=w-;wl\b:y=`DS|&'r ޏ! Rgiݽ{ \oУfV \?H)һ?:wT̊;@")pڷv0>+B vdohAЇj_׾1 h_CETt~9g蚡Aw8.N|';Ia{#\@%up'p i @z4 b՛D=8XfB #R?@>}bj_+kz}G?|[4 Јȃ'^ Q|]&XfB.'P5 CD>G3b?A9W!Pq 99OCH?8Wy1DS|KoAC׏&eHV:@׏B }+µ}k_04\tKѡAf9; _;\Rt~p ~;P>7%s4Tz O0l8oC4A s9NǝlE?_X!~}bqY^Gۺ êU3sϱ5O 8A2׏ɜ@E֛8\oQKOU ?7~q`GTIB!f#y <U G$y?7`5X[@yy"c9. QDPH!H8 ˉ&ADqyH_?&ɉx? Hnu9QAX'|}lNtC9 }$(? ;@IW?u>WCH;5pH9G@@ Xtf:\?}{tC A?&u1 B![mlKּ,? !1C|g#91A$P@7p`V77P׾LAEu3E)UtLݺ!$y;@3맟{uOW~omba#ŎK^=>?:JyW| w}cmɝ? @EXg@3O yPkG?&W";Y T;t7mF]@RN@k: b 0\1R)V덠H>!`HuM #`' ~x>@N=2/y8{J\? Go~P75 'APwGOB/p(..?V[!$Z{!W$28g" {$(}No k<"izwރwm`rA'\?&P_Yrtd=\>Mԩ;(u6#sqߐ㧚Gd$;?H'W }*3ú7Y nqr&wE^(7뇙,GGMU3Vʷ0OP % \%MuAk_׾3M}hS_(r,9wrn#!7~U*pv\R:7 N&}Ju5~08gpά_|ǠO)=2ֿq n83R֙kyCe~3cr7D 82tO0أs[ӳ\ֽ˩Eݷ3Tz't$<"z fx$Ĭ?I{@O=]8@GMC] yaP3HaXru:p >O}<gD. ؛C1Z zʷ8_y =rȓAPj=*f8׸b:q @ àR[D?R|c}<!O6bܵ|=}W5>>J> z{LH`QCZ!飒@x'u' vxs:H/:#CyRO"o,;:q㇚ }Ae[FvwO?Q1Sy?ԩzQxҜG??3ot׾LAW=R4詏M_e|l-p7 r nFP7~WQfp7 %O i@[;oswhV ǹz^3_{1]nЧ;y  |.L*ΟFi/>k?@ pR/9Z?tʃu^:3Or0 2%@(Zj?y4~M&w 4@( q>C[@ %wδˮ:uoI*IK r2,$&c[؎g'ȸ4Ui 4 x!$Lǁߠ~ձ{߽vog:GWֳtJE- Po;xn+kDTr8Cp:|En:? kD^CX@3ωBP!PH`O}ȹ㹮@ zȉ診V"Ahof#ߊyA |'R=)o;@HPwgnsn?kgw}CHW1AU> ؃pQx}Dt'r@0k߉H? Y-ш;(@g0sQ:#.X'=jֿ h2g {Jr珸|ҵo IzԢ2OlB]zڵpk_׾ n0͘z tiwbs h`M"ֿpSg F[?kU9E91(s]n\L+Mk}޲?R[slٖZ\rяzKwž.`<R_qup|O@mECXڷ>8. >] @}qΡo`n?]@%-B+pkz?e}sq!?DqD0ONyYk<VT{$JHw㻨="<"f*DD SB@g(@Hk6UoHw@:Nѥ~vӓoy+`HpzY@-G]2yOER SL9|Bzp "+"đZog }C2=ìrk8ϥ]l^vgy=eWG$~ڟ =ѧ?sb+o!ֹH=k w~BDUǏ0/=B9-IZ7}uu7j=Zw;@ QA[Ծ};@"ӽ^Cm7]ajoà~b՛z?(<C>ʅ 8ө+U;fѲ;Ur? {ֽZw'O-ik??qd (nޏY~^qN_0yYmsUUq#x̺~"??; |c.@dqٶ}ݻýi,:PXV{l;~C!" {@ x=r׿(8 $'9'rb|#'eD4Q{ՃQBedգ7\@A׻o5*w5p!zsv%CM(!''jQx>)B" ׀uE7Pz8<@*@&RDOnpb)SHבSsbbϤt\tl7юa(sSݓR^> >jCC>/AQ' L3~*o|q0kViO}d^ypыH_k_׾mP_3i9qqy#Huz9^j9;ͮY0w)s\ZyY\Vd=5a Pw{/9rA 珯 S@qOD>MgԠ9{ Vc^Dw .w'dpм;zcdΓ{MW!?̢N\@-"=qx& Qyw.?TA X<PּIn1"2R) S=]z.z =r!H- ]@D (kOt\$z RSB1+Bu>{'zE6KW~  豹[?q3A.j!`PyX FuX|S&CyhNlt8D?cG@c=GI"O7?kw%S{FvoުYpQTZW +7T'P ^/q! GN :wE/@'qHoe `1Ty׾i/jQ!wx|x5D qԈoԀfSS@@z }1\PC^z:q.G[Ծ};@%oz ߜ=7LY?t\;"'k=8s)5Qw2sS:}MM\߇㤁*@ V|@og77\BdurAM=f͵tkotǁϪ]~(@E\?&[9u8Aas(z??jvBv)!w{cyDfM#pgMEz8jSOU7꬧y*Y9R$*8zɸ\>`'5pmA@M@ E rUɹCCؓ}cV@E|:?)[@@A{"=1'@I u! 8<7@z (!XCOH=9){[]P\@QoX;?(τ}KGrj?!O H@z玮A}ͻyuDOk<[ "\W彺oQ[@uo̥''=q|t[D;D̵O}(o!)Qv?O\>yoDf8~T ~|=+($p O8$by~G@u أG?^3ŷ@Q1Wgu;@nDqhpSGZԾ};m |FS̮ͦUuz}No4:|J1F?8zޮoPeb1rʴbO{ |JsL2~3Лx(ֿ]Xg_WVqϬه lcpfd!? \0d5p}.kqGמ|;k7 y[@xP>k> 8Tw$$9sqPKD7!ּop `Gg9CҙJ@pUZ׿  OD$N 8ҕoH?䍀8}/k 's#O̴y3kQ!C.+}Kn|CME>* Q!?ҏZ?(:~Q@^X5GoD9Tn4u<. DON :8>(? >O6tS__p:EL)IxG#wv) B2& T TB ?(zX j*F4S N!G?Q# > R!n!Y浂 {gsf r@O"Y !iC̜>5dC :׾}:Ӵk)Ӻ?6>W꺷*:~L7׾Q٭G^otl L{ԁ -.xrm:'g A w 9P1,r'PeApx ȡE+&@ [ ϜԝQ= @мp̏?.YX֗9б8- K `^6\?5  MDr*+w){@Uo{duxb}k"(DM Qz8,W@Ԁ ]g#< 5=?9rۇEO> x7bȌ܀ u:Ü\T@tz=H`}upMɁODJQ ǔz!>ܤGzzՕo801vc˦hxgj)ɡ} `h rqCs裫ܯ*=J|NHPV!@y~QAp# Hfe7ʌoc5| Q!PHjI/_G^Aw~&GSwuѫ¡F GE_?O\{|@k_׾ 6}k?7:Ϩ9s9bN AԀrPl cӽ~Ek^6l[oꔁ sc(zdo7_Ws;.;bS w]#?M#h ?~pb5W¬|7zwMG;~F\ q~Y׳ dr3Ϝ?cKwY-qz΃}rhVY:\@(A|CO\?̀>>}f9!~,d[HA"oԵ8>丁d}0HՃ|'s (DǏ@17 {@gNz{qS @WR@GpyL]?Z}r77d[I'P.\>ԉ?.$߈G\>Բ:>_(! Db%\\ODTRJP䫸L]@QWGAOW@ !p[,Rs>oG.!g.w~vvG籧V* ҙ"xbЇBj"ʝ@Kw>QGtSSBY-:vӼ=2ĝГ=*?lFL3;:8rW5P(@P~3\>y7*upc9k[ W~M@~Yu! g ־)٬] ׾}; PtiPV6sxAPQ@ OS'бՇ>ݻZ'k̅O~xJM挕 gԘ0@(ֿ9 )@78dw-},Zndg͋E`.Ϙv̉p+,r  .;tu۽=ݛ-Zy[* r&f- D]>%QnHNB $ZW \QL yG`Q P-Vw9fɪ7rB! p\ G@z9,wg?bpb{@r7wON.&f >!uΗZ?8|'u4\G? :r ' DӇ@ĐD qxA H}1x$N;I~M ӕq|4W<pxpíszɽ獭ن7zև޷:=k@o9:"=SCω 1D^@T*Sؓ"U]'(D Q~f'>9 qZz(ue߼0cY1 JKg@@r'jqCD [-H|Vg91P uVOqԮk_׾ hti@3Mçgu}zK @, y.'|OU^8CP tA5 [bnrNi/?;@? q:+Мh94AꣽяǢCP6׏7+>hwoyۧ;xC{ō{w| oTȓ"6'N+w?zGr?SC]vfr'RؓC^Iz?}F Ob8~!Ѥߤ& !J_bUxKz~&W+H]?%rJ> ):r(`3>}`дǴfGE  p{ԑ5p"Bͩ]*8>>^#yDYMDr #n`ǣԀ jSFvXÃwC@|p]}M {k`BbxD^=f) SB8AZD>{d3O[H=W̤R ׏DB[D@] 3r O|;crE/=@CT#K||PS,ԵXN?8"חq }ki6z Ŵ?Ld}Sp<)#Їo<;dz{SylSS='1&g{7i@p,5̢}N|.4 | \8O@'VX4ӗ2Xoъ݃3po:Ꮓ  (a)\@=`Ŵ{kѥS{:ӝuCw7tg#w}'P>0wGAY=|2I]@.rCdQ5N^P AP#KR~5VPAkJ=??y>9B u|5eba'q͹{PrE ?y@ySD=JED]5n )Z@ڋ~=B:'O*GrB8~$K: C @5{3s\G>>C#qf+#OhM D"k""zRǏŸe^C. =*C3D/?DֿoQxAf) qE YN yP's@PH`P M@OKɕC=:z3c~ @+b[}|L䬀,<Ukq k"QqUԾ};@oz g _G | }>9 B^RGޢ>;v_cy,<W3}бw5xpWΜ",7hs"Tq@r㲻Cr:}׏cfCAg<(!-,2 JA l;ljۇ54t>s t&}s߱;v >( A#b`uPr}#J8{r(߬&w$~<盂͓=?4 p1\>!?)X&O\@Q Dip WCG7uP'8?&AԲ.@8K"4DO\AW+I8<@y~NC<>C=?jrj*߈b^8{T̶zQe% aq2D??hӗ-FgԎ o zJЇko7DX\{<񞋞JY}.')!C_?Xˇ\{@b!ңf=<; 3PO{_#>}ym9b\&Π\!82P|5৹pr0'f)\<`Pj_׾Nkƴ lݦ?T5 za՛DeCyoz:_{>ih:Hz>ڏsk֮{*mڟҵpv猓'gcfr{u'+[^8s mSC~k~1 [w R>tps}L1c=]WY7o?~IܱZ街>5t:r==YL:_uW% p,@e㇞Q>! :r('Aՙ}o<""`ր"zu>ΏP'"}ֽ?ԀY.~#g \- C)=p~p7X梮B'א |ȁ 5p(rztш CMO PAMAsQM{~~>(YG$Ϥ飘7kJ=A @ ] jO׾Acvz 3Qi۸9!9VM>=YWի;w=}B{yݽus3/7 󎊻fX{lj`%oBAoZ[|z`t͋~藺v;.5_n?: e|zߐ9f{WuMpJuow)wPm@ rj7X#*3r@<2OM~YA!*\? 95'rD-'YF !h_]?䅛?|yX =Yx󇜚~tFZG#N"(] !q,r9߼yD 1CO?tc6[j> |:3j7g\7-  xR~57=Gu!{ȫ7~sk،Mnܼ^8{+b#5:q+Ho2xnz.>ɉ$#N?>;@ >>Nuыj\@*W* jԈ x 2 Cs!P{^\g{?ڏUoz)h_`P7rJg|@W5.jO [ Ծ}; ADz57M{Ŧl XWq=\v^I#{[Pg֣C{޹Ņp-ZozkoGV^S 813p?@WR/ .-4Zb9ޢ'ug99b껂,7H|3ͭ Og׽xgrLgΞɝ?[ +~}}cr3_ Y#WO`'byD#@(γ?D PbM*j(z5G Oqҙ@z5p?C}N ru? G,;~93u  3ԙMDdP9 ?x\\\?OD=䚦:HA=+rYG> 8}N`!r#p(|[ΟД OeЧM5ҍ\7C }?4a9s;_Yy} QAO:yN?+̐u-{?.4' Ծ};AZS6j3pl GOr]=jC/>z'Gȯ6v6~[ jY>oaN@V^ޥWelAe;z.|ݠ>[60dYl$uX׿cbc\Lo7?T106Ki{o{{}c' WwqY 班Nbۂsf=w߶Ί=B8 ޛ]ve狯o?| _{uܣڃzbnUrW5(Ýr+A|'w؃(QCo]D+@g1;7 ̣C򈈚?ZsC.Ɂ> Y\A=kT x 4TI>iz0(8N78Z]|= !5@ v k|V.+n5(鹘7@4_ Cs\@icypb׎-zY655v3x!/xS?g'fCfzG z Tdڃ'Rgpw}C35pՈp(y"|C }g@~E1?!jqp'ź;DQͥ7GlPS?p*=(\T'>^ ׾}16h1hvt ޜkFF$ (jֽ 8s:D_`PD8om>cpgG7G =پ>|z}9Tҵpg€{{S{cofi0d9y.. ?)2:ֿqޢ'g;?Ek^0ɣ;@~Dn'̜>"H@'nt|z[~꬘6Y lt&qYN4tgWu7x۰kMxpbxM߹cn < Ar@ju,=D ooj9@R$Q`P@䀛k(=*5 cÝ.H;^SC.~!I$g@麷JIzA f~J5y@@"fpop xYN{cnҕo?!1TL8D;R?ܩP쐣SH!"^;@YأysP#ᏺ,&:p3Ndʷ8]oJ;?3kO-(>E(yYшA:uDDp&"]&=@9u*8HUoȡ5Ij\BP'Y渢.;=* Wd SA@ 4;ZckɩPGN_r<. J{ Db rYFֽyH|IV1e৅Ak_׾-6o56g`* 21+B$ހ?3;-5 'HODUEGk\Q{c!#*9/b]Og"{:yezb՛@!Eꔝ?ߢHC~ >H?&8ݹ67?FWm4|Oou G)>1HO rG@POԬ|ODH:~Ak>]VL#y 0J@ yb੅@ [@ 4~Cܑ;@o" *8\8g7bfsqx@k_׾?R%Sj:-A#. f^H>Riy`Cv~~L}LLPC-o-\Ws9Ys@"Zb(,2` Ӣ_K G@.>p7Do;?ckl鞽wzxbOΟ]>. @7ga>| @PsY̩٭?7;Ƚ Гn9Sw8Br ɔ r@PD"x:xH0B:8">Ul-k@jњ !@'zK?GkW}k_HA:ӔPLggB<:5sB&@G "Zo=r8^M~ MQrlso9ʇK5 L+`ϒ+elXدuM|_ ~ ,2'>?&=s@GN ֿro͠U[W?95lnች~@@86|'Hh<ϽzáM+;OupHBo&U<?yz5. IzD8<_ ,".C=k[?A'|xUg一CC.=xP@D@ Q`% @5e'WE*?}fɭQKta3;Zȉ x@V̑:85b \O}WpҺ #P#JY+G>cҩW| .] r@ySCO]?(?z^[y"L<tOSzh7| Yb<GA6=79߼ c tIo>-}& @P@ @v5[3|ŁGL<~E5+ӇYr>V$g ɽqޏYsC]sG{Zd=<;&<ԃ +Pd= 1rzGroL?Pk 84OJ5o ]8= `QAVOAs=uCEGAO98졶\AQ\#"?5N ~)}#GM`'D;>Q$Yk<|e3FgԎ w<vBwQ#ؓCiO]?PoPkO'$ (=!\NO!PD+3A'Gg }jV񽀞}==z3A@  $ЏnDND[7j`O@uoRgN <~v&"g[Av\׾ezko.[ U~2 \g8~ү@ rk[?(z{_4|A<>@k_N-Zs`P,3?-{vh ^?W, _[@cpA~gnҩ/x/77ovX=:4@wpy/>|ف _~Sħ=<5p@p%@1^~"6W|5 8ΟuzpP+!GtN>D] 5upHWyN ѣj/UD$Gv,X|CjᏮxuD.n:D G} W}"'BԺ di]F]U8{3'CDGČZßF ?Eu>y8tї[?=pEx>lyDy8oܳvlb3z{}>*>z(TR);}FF(*@:OIz~">`sAgm&ߺ(Ч u[RR)"FIr8)r, k ;@| nj(Rޏ֩rɑb#R_ }=KC ;@Uc}k_Z45S1x3$otiP~ۡN'x}ˇ$5Mǯy5<};ooaĎCgn3gעɟ,=[E=?_yLNwhF\`ЩxYeǕ%{_.źb½je7ٟl}wɤ%FY2ңs|M0WGjql ~>̹AkF.}~ߜkMG>0>aLyq"fS7SN޼@.+ ?XC~ek~+l>e_>连itG{* wgMʙ/o1wå7UO&P}#2poM?P{ގio7oIM?0ϛ}c. gL> ە3Y{os$Xu_uߟy%Vs6 OUN>\0z*.Y>Yn+G@wrr{0?"ud(;2{Μ?$⿚cylV2?&3˽L&KKe%$L 푇#RoTݣ942F0GrU6ܙÿYT)8et3AUn:?Y@v\FŲp&GAY?zk'ƿZ㧳:묳l6y7[zdH!"c#e/K cj=VfFm {#@90cI$TS 676h<2*?>Y+o_΍W0u̕_v2ylhOWI=Fub>5@O% TG,7kMJir^4{:CWYʤ~7,Zz>WWPG1lg'ӵE7N6ۣn:묳\+ n棃oj4B‰cxR#[0?9Ȗ'u BH@o6{=LQ.|*MT8B|d&N=W&s@>Ãl?dȃ>Zp799 K>K{d> *K{ayuأcN:XcR֞ Īƚ,QظAKvL3X?uYgu2=f|3"A,8i`Caœ $efΙ͝YSoYRB,<[VZދh٭|gܺ1)lts΢^Y# oW1U% PJoLEA5u=SwTZGߤ&I~-'ղݬ~ɵT) Bew rHġLOCf?xد T*˳̀zPapf:묳:7 SXA9LD4Lr4I'{| %Wcl'L,L98^U`ƑK+zǺXnMϬE'HJϷ~`:6b} ٷ$rbWӗ~ 6sC+!hOn:C5`w8}95eޤ][EcJ$שmg0q;fn^:묳:s'| { <+}[?6EL4hH gcx`+9dJ =76?\aE+k[/|g%F=RavI w*1Iôse<@ޮԼRif7c !¼9 nwIN!q4;X(%CAUg U`.ѠD_P_䳀NkskRSk;ޗ^=o/_q:묳:7c5~pnv6k%LY>BRi\x \=9.Թ<y Z}!rb@T"ELߕ8Vy5~yTC/dy *,%?̜5 NA`+rA]fn sg̥:\\mMqHsDuWd:묳:L~}{f$#!B@iv$xs =w8yv?{t ɀ#w DI4:W̡^nt{Hؿ5yzumJq9+p2zG}3@ygu7Lzitvç*bTIKOĹt(G#Wq4cA>d_ pZFrn [H&@譫C.m,s <ҧHJ{6E9=fp#}?@F# =P/+I @Yg)yw+-yPJ wY)0S>G Iw[/ 9B?]uYgu ,$wgGf4\ [`]Pckq|r׎CDȬl>yܷmM"R;Gql<3q%qa SX =2w8{\Jrچ=OMV9"&.Mc}pKs r8H?"p[X'ݝ篠0_yz;QsA-@yS(ܔdDxqB)R C-Wir}:Rb>(UڏJBr*6.|lUJ:aupeH_+L$Nܯ:묳~m?g=Q3-@${naw8>0e "&fEz~I%5p NT/^d9|sSo`un}5`Oe=/{goKY9/ ?aPASHdP&-g$(Ro\QUk@cb4x eeLP(*UvPi1/䣵.QJ]~hʇ#T2zi<韗%TPqC,ЦRxDFi2-';z.r%Uiۡ_KȂKsPUސ Cktkd׀ Q>yu2FO<&\rQfOϾ/ gtp'hrQ?RZ@~uqǸ~75o])࿓z#x1Q?ӱ4iߧø'Af,yDv#A42#e.~VswiUS:2Լm}:}Fz\mvoj*(7[`#Ǹxf$+(X/edm}wQ6\ vOguYgϣ> ` ypE ?l6_=hRa![I:aԏV9DivbDth>‘뫌5!T`ilp l228 ROr1+ԲIW{>c;nG85߸xhPPia7ثMJRhc@ou&"lcQN>ղ Ry6TrbIr)hbqUum6#%n! ٣DGK-Ò`SվSWs082(Z]KxgξƛX 1$/k${lHn~;0MP&&''>\q݉|RcJj9 <o4ϣ!cc xOOi=VnNcbŠ"2`DxƱ"&SFץDYZWFHeK 3܍}R_(28:rԗ(Ӿi( uAX:TߒB&- `-?0hqMuϋfa pNh<h$̩a;r:P:묳_H ,rK:AdDa5,L5/<{PDrge?=~<~6z:%tɖJs//TX͍FlSX3X_8Kbe}S&6d?@"E h,qc+mLcyfWnmJ!*y8@l@;~K+}?`\\R7]9uyʑO<uo?/Z Q&'Ǡր3?TLq)d|5L&H")zǾ;`A@bs% {);r}yw$ V 4oD$R&qu/zNJEz+-^Ysc,*)l(ouq^@3ق<:K2xg,X:V$(J~rc*CG6 ݾ}Ox%2,F x.hr~ $WLlq_GX1 p`]0y -b,@،d~\r[OtYgu7q2?W2h!d1~j"v:GЊ x;}:˖Hn4ts{X-lXN.,qhb]*cG6ڸT)pB4Zx_3(VȦcPR2ʞ wC&g&IZnҸ>|Ҁ@?\=W7𼓭~<\Awy|tGFoͽ9o/.0_,v~סoxr& *5b9.>^OQrl72 Aw6L) ,GCeȍ:1=VYl8쌓oS07'v;=GRnJq)Q4NN~fp}MK3;w׋euT(vk1UA͹!DEQGiH/aVOz/z P(Y,PB(dK72HIZw-|SHN_VpQgI=YaL'`?q^#ai`[j=\.`]0'`Q.UP@Y|?^۳OW?JRpۧ˚qm]jΐuo|y@_q(s69b6gTtt"COoW|l%O78^ܝ \guYg.O>` | h1dN«SW<}YN…_ Go!tt;l~!s*U*kGN~#~T֖G{X]5~8s^ VڙDtF&N "k'Ⱦ#S嘔cL P!ի f'KsY9$PnpOFS&Ji\\Yh.kj_rq=u K|fR+7GG6D,+ ''ρ(}tWp:SO1O}Tȕ2e@m$}23`cW|,D.zuq1}@[ؓEAwe;/H|=Hv9*QoFl[ˬ KFO`[zc?NXGd= & щ~R64˂WK kq&xg8EH$l eܱ?pqG1sR|tozvz ^'n7mؒ e-]0(lY/l ukd l``*o+46;e䇌[@ הj$`.dޜ} S}>ַx^VYX:_%ษ}8Wҽ_ʰRrAqgMTI@:묳A(U_#6=N6zbM#áZϹ@‹bQ0! ҋ皯;mϓ/;P?@{Cͭ_ =ө ݣ@7_{iSXofsGn Gr1'* (qOXḄS0_V l]ȽM3+N9x6nRӫݔ*U`JhzU˓4y?tx+vGrO`s<˞Ʃ TVfn?ZyhI1u#e;%;O$4 -75]9VjG26|PF~8bQc`tu=1"-J”8Dcȝ(BC9;lX5y~aJXr%#>fȻR'V@bSϙ8_L6 d?~eR x9,?_dS&bL?44$`?w!S}9`T5T*Vnpz@q|ceE쟂d>NOA`\guYgocgG|Pv|7 6-+hg=1|pB8iF.Z!^[nshPU:Jn?zBY_L(ʙsea8;#G6ݾqr<1:'OC07@( WJ##hmI[WQ6lrB#W3ѨԠ=dL2cm}9g0 i;?_A$2L@G?j;\*!;vwI!;}E';I6YguISξ OBl1A~@tmú;x} }J!4uZDx5L??wՃ(%_ëo9:ĩbF-~^kma*;׶^2vvد "`Nv}6RUꋯ드AƷeȷ/Lcy[M;y>hʸ#%Ja۔R2R)VC:ʢdָk˷qOG*Z<~E|AŎQ:+~havvڀeeXǞr3=Y THo<%m_-IZ(%mޓ&e$>Rvo>i8h[U-e}C(YDR0 v*蠎pGR@^Ε1XY8Dpc_, 'Oc%Ig?;f_XVU&m082~Xi%_#F*O{`Ð<[&7#i犯&͂M ;;usZ*H!Sْ+;lI`?Aǂ1LX1#Rq 5RRPUp!bCy3M9[:K_jY炄N} /Rݜ z(0 !p~3YˎYguG;{8˦?PGNmw0i@'ERвH@NM h=G$p{Ќ:r9`*_e~3i=4zk쫽59<; hT!?(FH= s錷7@ٹi,oãfD"s~@,v}sϖ36Gjq`~Q篍?r|90L =4V ͝`q"bd˸TP;2.6+ ߉u>V.}Clxև@>k_P9*=@lZ440q>7c[Il9"CfFXԡ}cfQZ_srYO= LLV2F.20 [C%.HЛpڼM[0>wKG =Y1CQdd002?A?je4(VH6P}Y7WTp[GYxA{~T Nxhȍ&fzr_0 xA: 0 *Y(7Jyi2Ǭx{}m2]YѣK?ff`p $-$=#=-YguYiOE߇c@q e:&<6Gg$#pfu4귃'r|,5~|e׎y O*)](g6$1h]ֻWw|m<ol\yv/_֛;C7 }vI\F@) Q(a*yye*0M~~THZ*%UvA*NyA*pźC-oaN_d{I&OhaJmgXom^ht\DY,3 &AB L h*I e(A}FgQ~^ =qsDd!u`0SG,y4U=,Hds 3:(>$u:wD(K td+DR4rV.*nFRI>@05d?{jφ(|L~2$rC07J1t?1ݼܹ5#dwrtc5O Ɗ'Z*Q+^o'G13XyFn] 5^|Fu:2ey\&P,\XØVQjc$pTCԓ^ǧ\N\S L)Ծroj̎ƾF={oGit>~8D!DwL:묳~E*Gl#_C0pxԗT[Gzv~Oq،w7tv#F)Ak-7NrI/Q =^# |b'/BE6ZkٗM<ڿG6ټwfsK'0{f֩ ʹUZ7JUm@o\>IѣWpv9fdSQN=J>l!ZMGWϿ{K 27hN˛lꄰ0?GAa,lnYI2!a@oyD'|=T3{Ꜧt\L >{ʑ*B,H\HQ}}eϳΖӜtZZFZ,mnZXNſ<}H Dz}Y>' )Ҋv߶M=lrkA4H@vڗh%}\0uB7;`Ft˶:ŭSp̖w)b_J4' oG0- f8B &GjUI;4 dWH\mNyVC,/,:|ϰ/ 0nbMFAp[]SWCIYx;:jpOP.Zs8 :gFrb㕦`RX*,3e14hm8Vۣ烟J8J' @uYgMwT*ٳs M-xɐJE Sm<zf^R@W@2jzQqď)4^1dIZ,}GP6;iJ*yr}ﮤۄ 4rKdASrJ-GK˹F~F[>sD" '%QX|rgOfw4}zu$:&ܿxi Xd`r˾"QUr嶎 8pf+s{`zOjLR,d1*a䇮ZN]@Ԇ7G>ԕR.j8G:ŹbT!<2<4/y' r& V'xA$Y'@cg`:E>V?Z&7/ll1|P>](!y4_<>'2c_ 8޲|cH{PD_-Ms>,Tc<.J@ #RΑ2ƑԋСyA?tKL*JCnN:g>1~`V_Jw6/ 2xaxaMɥNo>˾Q@eS\o1%3jk9_%L>gi?Hi|G߸N:묳:?jhg13;=mzBnTVp맷;S-O)kJ I"0ȉIztlK玥Kt;/@x,7(я?J=˽ԟ?f4}2{&˂ rF޺}cf+s[86ZKo >\4m/wKYAz\\&Y,#h޶Pr=z~ԵSVɷd|hɮc'gNsJvp|V6oytTz ETm1X(Wu X}.#`Hslj;uNg:˄!^DuWqD:eyb*M1%S`=*mI S };y)q[߶}*%sq(y=@ !L# 2ΓI)~.`n5҅Z"tA*U8xGP2;OguYg"gx?N]e)l1XaQ|x:3'A$0I""]>7.jLGO/nFQ\F[Jyxt3_KK-RM``@L(VzG_O[I9UJU@0קd遀cm߽l豋|P#W)=c˧pcLaйuMqH*-1.NY~8hy2;ƛId)L#~b 9 &!KN|UֳghHY'QODz:&BI| xGio 0佢>$w6K#s \SVߵG8Z|[ƍl&P2:``'Ző̱$ٮCTInۜT\b$c<<>sA)CtS{<+^lb>.^5?){IL>(l'+c,e+*_ζT)*kĐa>쾒]Di7Y~nN AxV[&>sD}e\[JK^>)Z,2yfG2. ncvfn_| ܘ&y~baro> AP Tv( gG)rfϝzlxtyۉt~ OʹG_G?9|dl M%mRJ+ dE?c\uΛyۍlDŽ/9QMʹgtZ$5e2wY)}uf9I (%.YN }:7,?;i͊.|98$mi蘑4%'!ӸR 㘼鶴 T769'| RN5 1Dרo T mwo#WkΚk GFvo ==ƾYn*k?rqq}#fqd;d:o+w.Zx.bS0\biaO.Η1 : _U6ƖbgM{W[);;F2Z? *H/^`Pzh)uky͒ciTc|iSj݄C >RuYguv> %[B)XEJXRM ƣO7[SCȌ2/]zzJRmYTǎ_^@Be 7q|G8ی=4NnlՔsՕ[i0vY®J+77$)v{kGG1x,:cTTFn_ZO3{rn `f`Ac$tK{7p\ :} g<'4>Xs8\i;=&s_yܐo0 r:7t+l*c8BnNQ?1x#,k ,YPS ygǩAYgu=~]gިy~~ 74ky:%?*>lw %FՆp\~M4XѺ@Yz_ƃTR{`6ol{tUl'7Ԁ1_Ge][:>ulo9,?tεktPD<:&y~u@WOakqq}=D;-EcXߧWrAp$I>,r+|bv t|, 0 $LJѾZ9=J* 0˸DZ*I TF)2a]J_ٰF@W%+۹o90=#mL,9Ķc`eDs/r%^tݱ/gjg]1mHS*CVyOnD!kŝd94m)Y$Мdotd2X 춎hQƩ5-dh\2yG3Ρϸ6xW<1s[ّVw&x0aq-DYguٯ*6/KDkt[LڒRX\wm fx!ٛC'&l DmEX7/x-#rdϕ {] i~8;u#ܼ^X?~< [SS{(e.)oc82hύ>y5;1hC?pk˧ӕU w9Fʼ6fj5R:|k { I¨fqԱb-q!BE. LoO%waV,eG 2=U'9>RK#0d휸 @a r}7yN-}~$2*w1cfs Sˎy2H:N~Zwγq25B"`UPpo(BB"Ε ^<9ZyThtdzpH"ȬҒ :5_ TMܼ4o%u&kA[IY0WaẌܖyDR?Y1{lS i^M?xf.C\O57$fW?b7K:οUF3s&\Gi|ue)~SC `dyꀟ:묳x02?ݷ mC5", )<4rp`$ wI-4?,Y^s1.]\-?tiS2Tm.³X[.meU 7n%l9kQ-(6>H}{{K?} X^VdN'NWoA=٥O_l/[*6cC?_ _gO:} XA)zU _ƌR'9R,|Sz(i(ml 7cc[IPIcu_x#>rZD, ;/TĞCerhc ts}WuI  CMJ㱸m95\@I3[S'D1yT"%cQY1_1S@S,\B`XKM&ԇAgng%u5ˑzmp1>!.y;*!:A?]L<4p=I54A^~)w9?A`iqCA de# s;7T\1@cފ*b\E1)? HK>GNxzı̝{:묳#*͗v׷p} fp@Dm7BGc{?dz}sy[ <~Ǐ~*FtH+_É4N3SF[/=ƣwÖn?v?i_> ^&L#WN-lB# 4͞aIo9<{F;[*.>J9<;J5^͏Ѕa܇Ik< 3pm*af LO 4e)bG.yHgu=pzxb)#e9:5$Ƚp$ >.\OLZcoRng:~(8Ĥma'v饎}ղoYD'{mE2vzț3YEXtx 7aH0(>mС=#c橚욀6}Mr@wB>ơWlq@V-ئbšd3߅ZlG鏠1n0"?xqQ]p5$t@q  O&Uǐg!.O*#2 1[ t%RklGdO$`c6c`h$.l-4-捇"鬳:?k> qk9,Kc4+]]1Ckzlz=q@"LHw8HMbKJׇ3KG_OJRKHYuѻ_Mgftb]xXnlB޼s~~9|b#m֚*ƴ(ѿ1aTO,I='˅֞ wq46F rM[}˷'5z=53{jjw@0@;Җwr|i'ȾOXgݓ\*Cb.pV\-)V 7ޮs~fs}Xq :JTe`}L* dl33q8ޓZ06I'/wt x,VO_&?XLe>kIxcq:&;exyC(%i٭VP7NIV6Wta_NO"f#Z]dT! }.EOJ+'*ˤ,MmrlLN_9o兒y>_}처nPCF(s%H]٬d Pa 42~Xl%&J= ؆ FPNAESc%")(??5Ƒl#|5*WFz_x7N`'ϰ5eJ6ԉWɅmnSW <ڿڸqngx% =J_zn;9TKݓJ3.TF43zlǣO{=Gn_^ޜ٥C}՗~ݢxSd 9<}G4Y3~V=GvJS9hx0< BŲvG/i~Pt,@^>jܑ5R >76}$JN %:d>e͔Q|iTR,0cvjK9{1{FHt Y&Sf'ԥÔ[vov * pxԱi#ӌ1:TӖ'-@S"Tf(f:k f;/年s3& %Vgd%7Q1YB|2_[)+É0}?}O`9l_xy1Iv?6gFjzLPO|/IW#y(\d.nh)׍8[ԩ϶ hRpH`mRmH TfFJ^+bX@OK%Ok<`@O}?~8ZW2.MHׁE%U88uWJT7k9^^w&n9X /UAz<swH[G$EbN*{f|;>X; ; iOW1Cۗ$ߺ5 6ɓe A G߃)%O$.h2YyLV%(o:2X$X/x9Z<5』fMt0NLT:L3$WbB2~j0'˸j?ptw鬳:?jhgjWA hkFj@RG)<[8d&"&g*/tns҅5IL3?@/9VMeĈկ_ęS@cL:Hhj~4a2f0*c:8MS2?֫OEdTGݍWKÏ}.?te(lIjzݗFeRI%RRj*~A*efiOyߑhTOB0a\ ujĦ Lq- \5Y*t}H[az2rܐm+&' $SCn JQ:b#ccyҴ\rb,0 ysKKø?_i|{3ciMUCcKԗctT]_Bg^)Ĕ-瘶O|JL͹'sR-!Qn?"S4 jk 0> ԥaY\0Dӟ_l/bxJ}%/["eɶ s $ڡ!\g?܄ǭLsol8m?uYgEg2~B;ߦT3@yH>\ez3#ў7N)`9JϽ(PO[`<=$n4B[#=k=T_=6P8^ȵW T{ TJ gǿM$'e}oϸ]F7}v/_.g$%(O}^(]d :eϯI^`GEd<7j0zD<^%(|py~tӠ!)?޿\iLLy!=Ygu]~~3m k`_pAtgN4)S&gfxÙFMmb3%Hcv뫧0QRJ*.'}+e}n6yi܋X^_ehmTVO ۶B r8dղ?C㏑j}ᅟ ?}01F}׶fra3l^: /|̳[EzVNL=߱F9Z:*@SR'8i#l#'Rg>G dr"](9ƶ(/?)FN}ä-- Rq;h Bjӱ3ۀqrƜ4rQVHOrm*31chZ[j~^)@A) HK]\M$}7T}NXܸ0m`ϊ!)ŚBi9v$VM~SyX8o?Z*HIꃞJ.51s |,Mꙶ]:묳:K:?s~;asPGMc8]@vQ맶}zIN o!}\fbPԼxk D,z9tHe3:tM"FDiWs/TRU'vyk0?̙*#נQte̡ggoyQ}1|~7sぴ=1;D}h7 [7w=*zM}̏~70X=C*FSduR#"5kZLp'K% d?^h?ƒ9f{Z+[n9n|Gr]u}Qˆ27) 4?"x3)/ÓtYǩ.gxxDz--H]VGJcٷ<P6Ro$vv,ex|f>K=v}Uf-f+8($T7e9K0HT>GcVe{~<9L xo;@ A*g 3͵B rqw¶~#(QA@E ;a,٧>ؚ9j??MY8~s֫S!h9sX-DOn]J>@X$ ?pCUctYguv=b|=7p"n=h%}g#4cO"pƁ^?6F(vgn~˷ Iך3o̞*Vsevᆻ;&ބ Ro<DF{>'g4M~kG]0?rͥ`>\k^pj[|O47wր?OFI?Xَ3Vv9ʪ6uJ\E/VmtZDk:mӱn&m@`m>bYL,P͢gyǶ?Mhs?=fYfZ3dL{NbG36Ȳi@N\5wf%2dZ[ KMx䑶XYGmƐ<é.}x/ϦI TBDvרfq!E t& fdm lK1=!D2NVRwSr3X#缊RA1t4͏FL9o8X6^쎩l #fN<}Hz,Y98gq?@(K~UY.g,C&Kd1S V9~:묳:Oo8YĖ%?jLt 4ٚk ulnqߙzBtub_Xک4B?/ ?r9<7 N6/]Ф.dY]0 @Me[eM2?ӭOz$Ŋ K[/ߜZgsf4O,2观~v abG|+N#b;Et6wwq];AT$Kϒb+Sl8~R>YXKX۱%SbQDbDQYm&v4oz3A&¡;5M [&$aIBb0qmA"|f!$c_`(X6 ^~*Sx~GbX@Y\|WӶx<x +lhT]zdN6Qv ,Htfaٔ'I4Svf,Ye8 ZϕNbJ^_YVV!.ٞ  ֱF78X?v)Ȝ -Yu{hF-$Na&%KSEEm@,gΡ1NqL?APoĪ)l:ŎfB&i)_϶?Êğ/D7u:-mqB-o$;F q3'mT^k怐'/<}tu}C;_v٭GΏziL@6}TzW4fa=PQyק~7AMPu=4Oe PXCe涖C龿7l^WWNْZ:0ui®RGOTg A u}l?8]Vzj05#1ۣ]܅~(&Is-;$e[V1f09Zf%c 1hՌ!',XN%3vsltS7 ~I'H:4&IY]}ui[#E#Y0. ŚS` H$\kcǒW$1!\RJՀK/QOׯԌ$qw<~_207 h _v { I{@cv,dپ{Z;m^ rO @cP6o1yq_a1OaI`)GOl8룜Tj >;JP%K{|Ltܐ #R-uMTZ*nt?n\žIqix1[OmFY<h sM#ǐ%qXYCU}Gֱ@Jˁڬ!*"W@ܗ7ШxKGӵ;xJ0Tw}ŷtj-u~N\rʩB(nȿyS (@ 0 W$%En<[}nGnD: ['|';7, G|T jֿc2XdidUx_XS-{Am`*iU\9H>S4Od]-f 8Z%yFZZJb0Q8 UڻIj0 VPt##ߗJ9ZH5OR.̇F[HI'8/X!؏Kb]aP-a?DnJ"a&J Ñ$`EԸa픮M4]ȗa uwdX{ kgmֺfJS8DV[cl nep f\`t@p' 1?@4~P]mSxWer=I]%d$+?ڕf)Lsb-u3~< $gaWT{(``3/'?2#k6h6 IO qla|@ F0CӺj³AC~NoF{|v|ͷ \^ڑ6Ewv}l6/zK/Tna@QlW\w\B $r'|q$?l𼀪3{wX6 d{ @d>~r+󃥻rW_p_Əw#!S@?%4?/tT6MW$LёB}m$'Kl綒dheK7* H%c9},:ʵBH9:QN7n.H8 7m%/dɑ0 !|%GK1 cl!v(m8j?bd%/ T΄f0y~&Iw(_BzS'XzRƈUޘ1냐 a"ZBYv_$}>I$-m)Js?EmX784"wm䮙I>6 f!CĘf6}Ct 0ʅ`y锸JҲQlNqimO>?y)kκf*_oι[/ ~ܴ"S,i؈uPnh6ڸGOY_0f/J]LXAJNc;t͟m3LB. y:-"sUXWo^:9z\D <>6}ag{Bޠ ӯMv\Ps+Oڜ6E:R@E-\O<{r7<ư懇wHII''f_X:NGFu^`} ?oPҟ M XFj\X2.I`Q͖Atԏ/u{0 ꌎ0%m*]s =lw ə((,[W$(M*ƑCB}Ҹ I 46 4K`mRmڍMo9A+m'^6|.pU ' =le!H>'ra Jn,N'*@̱v۾>-#%fwd߉$"(y&@갉(m.qvV2T>{8 5FA K`t5 h0Rfz WGGAU%O?&9|et!Ve-NJ|U֮48^xaUdx*Im4+F$wS'~IѾ]I7'G Fb|D )kO܌/ݎ>cEXA{D +tx#'7}-״ I'[01Tt˽1nh6ڸG8fq+^C}H((X·{ T=0 ;5/oqڎnl)*,8qrV}|W˳[ύqîE$h* P_ᄔx\ '>Qo7BB,kS5pkt^ӭ @l:zzcToa S:.^9w#Ovn#PжRvB';t@7yq:Qc%Y#C܉e+T@\@15om2rA.!w 5't*{ dR ֤6ж2C eD@|Eyžv˜"$$lA).[AcmmaLfo9k@0FgEB1߶EˡU4Yf Y8E{NbB!O{Yߝ>q׎'|]*c&e4KPPgfMhi'O2WN[QZU%Ӈ#jio@'^@PCˠP{Yj|Xvr?Ur!6җܟ.|w".* QS(o go.rqG>Tav>VNdmF_E#?2eQ! ^'GX۽[kMf:)+?J14rPW.ͣrH9( جwSkL͛c=L:ҿN&OHgI #w1zcK;C ~\6Εty 9 s@Wg_Y, &J :&` tޫNY zᗏ6>{UX&;I/ %Dw ofJ+v-,ѥTf9 JQj2 ܶ-_UrTy*&xiF܎t?me|EY'zٞfxLpmfفXFK` D$A&/ _n /">O {x>+$pڻH:m@I*g+I`Er}~s(yrc"ba,fz0Wyv`ힿ+zAy5ND,Bѵ#)v2X4_Y$(AGn)?wuAW% ndvG=)涭?f@ $ƫ~b$,0rX[6h6!ha̡Y w|O+) HE C2vaf"&<2%e6髣t:Ig|uH?ˈU<7N>ԹsaLzu߸rsnpFF2ia@RW>Yz. # ֤R맗;<ƹ 6𡹷?{#Ž+Z ~ )f/gKE9ZWZ`Q;XʛṾIldeH82̈́$ (gI iG~BO",֕vnPn~$Jۘ9i>[iQrL7bQ#'la?T@=/,r8zD{dd џI#I4t$P,Y 7Y 'P@ Q̒W S\\k>c򐶲. [=˰wQvȓ7S=t3!Ơ`Y&gO7gMk\SN1[,JxeWe`P 9tLTb CL$~W ʀЪw1ɑWl"M-w >?C;N9O{k6h?A4 ^p{"VVĉ>eh8–-ݷ出Ftk;N$|ZK>PŴ8~YT6`WY_~?;҇$ZNhB<7u{p&T_MЇ2uGFz60>ؿ-Uѩ5q+W 㾍 Lwť3u;`-1O@4,mJۥ7|Q\F~$sui,]ꏳoM]Ql'yiQ( %4DmnH'?ʶ슒1HzIPS9$$!k%" Y+ ^+T>O;G~' Ht6 bP6a&+hR=]lm!^J 2/gBWN3qp@6!,A)_3l3i\7@7b e0cM7Kz2>5# ~@s*vPNEzVzމ,pl,dBuxK241{0ki@{r.?gAi HѲbr%5|NTѮuJR?J9m`8:6O} t$e,K& ]smӤN?J0v3||*1W>wPqVQ|QQpB (y'w>O'10&Hivb ڼ;0o 6m"g]a3 >y׆$Ll{ cf-iKDdvA]Z'GlEW/:>,2 wdiVhTIrX`Sf{8V6h{t|OF{o'GJ&-$0dӽ!VcaW񠓫Ÿ:"f4'%Fo}bTwNS}`>7 Kf,{!.+R27&zߺR] 4=2}vtSYN̟r0;6W DLPƒ8aZ}}ݟ}p2 S@^WvTTW*@I4Qq~[&4L*Jo45*&dĠSTr_ꈓmtx%prVBډnC9䴍Yֆ4Nx28V~۶_E@Z.zG=-0mMjvؗc*V}.ѻQlj;B7S1bU;s)Q=q4vn# Ƥ$?_>MCE[J;2j?֯`P3]2\d @PN΂@pjd Əf&]ujTDm!On#Q?T`~nTj VkЖyCˋr1[ W>n<0+hHJqn| ^ їjaEUj[>qr:&_q4OmFysvYۻ)(fq) m`+9MAs;6hTAr7ZI(C]Z:&]tsh)!Zx;?uSBwޠ?R҆4m=߸z. <.>2[ygwxΈ[ BG8Ra=/ysډT 2P _;q:/LG2&]ul:FՔ؊I5Wp+q̝eT#؈ݯlov)*rl$\@|?zv]qCv vb92 i2{ΓQrDI&<3o  0l6iUZ~$O(%>yw#!3wxF;7ܷD ﰼsl~9Ԗ7x|݂ w`qd zxq Ғocqh|ڬEL alnay1(ڧ՝7}M%U53`?EO5xZMɔ t-8>>{b;/gB! kOtH^?tsO+Fm=~xQ4\{>}.+@Dr*9G{:C3ju;E>b.IiO>@RĢdVO'4fm=g~ XsZӋd1E[{rJ ↓-o)C>GhoapAٟr%k̠b}8 )]7 @0m`jF_oQ:6}Fm=~)'Vs>nZ8 I2>b>G?W{yB7($7m$@WX-uim<N)@JysmU4XjDRgZW#v~uksnx /@R(omhYNpibŽ߿H5đgz7_<ѓ tP co$Q5(6HˊOG4%dr eݲ' i\r'CRt)vXjM%o'0Q~mUs - %{HOk6~${R[$hKB>@ZG6stž|W$qbO3x ڃ,`?#P۾ozOPW:R۵Ӝt[ZzDI9`l|fMۿ KC`a1Z{k6h?<`~`ۋWDǐ@+=vN֘Չ-G-[B&$lZ@v~͋dN@F/]>y2FXp_u. Y:'U٫y3ҥ~V(uȟ|T"[yB{V!5ch6$ש_}x2 z{><.ntx,$>O` ၧ`<4ﯱOPA\  6fI ȴ6%fPB$].12{)n7G wD lI-)9ˬR=,^"h,¬Za@QF̤!o[N t Gڦ.LjB'ɯ =Sh`mI} t_{h&#%Ɉ &@DTO(ɦZ4goaJiS.*0S;s"F}h_uzӼKH@ܾۙk¶?WUa!A/Eh]: e@91w#R2ͥ*5+"*;mO>NU&@'8ą > $&'`)G4ւmh6ڸJ{ۅfqȽ]n^ 59'4pk 5q`:LMT0Ȋ*U,F֐- VF8_`$F-bjlI׍Z[(wQ$׏MT|J3yo`ИI*WINVh`qiu7WoXI ze鍯8~M ͋v^+!~6#9bo) y_Q{]YmҐJ\$fEu"yʢ$oךׇ@05B$&'(nIP Ӕ_ =W_nj'a9y,~HJ~ 76x Yo3"ymKQxq`Ue,όATJPv+,ӬA8rRL4OS vN+xLSddh P Uǀp)ڨb01Q"]Sq٭TWsVf\u7Ƥǜ?h3AN) lEx1i diA ck] LJ@mcqDqI^@`{ct5nhS+Fm=~<~4Ƨ4[+S2pTXZf9C'zH "/#5:Tݟ^/qUv~:ӍBN)'q]Ju>`S皌qƁon/r̺U P*Gb=h+L?2za֏͖Ҁ Y"f[N8HS3 *-68d>?6EFm=~&< *1CE -w8* 0'-p [Q-O(G`s 1{}z=QC˕ՠۻ\/O^URuFp;KեлϜl۔\imw^ZA-eҀDPGRϭvh_#?B-3%]2JG ;&W['~-=I!%ƕ.~Bjk^^ 8os<6@n[0MErumW*͂uDU89loDfʄ UK Diץىeǔmw|J$#|LLvj&Y$D_l D; >qjъ#¾D$WpKfFBM% IVfO+ ߋ%s,jb5Rg9`dD6EE|@ B&,ƥ7'"9{CyM9eJ 9.Q ?@UxrtTb}qa  I 0$Uo.EUs>>vZ—o"O YXMGFmqq_0fsq{[w@ZKHsQfeazf'3*B Ġ$3k˨SA]`KRZBFZi#OVt )rz*PȋP<tUb05ωMƷ»B^> ,3!x89SwN]}(x쿱&͖׵yιBU:o ؀(Jl$Hyp"b&~ oT=h읗w|bN >޼f v=zIϱa3szuhl;ϯ, _[ :${KiZ7~urR`}^ں /7#$pxx<2[&3;Tgi3DXx7HOc;+retnx͓>յ{^_bdMl'Tj:i\Ď0V[]nĩq0CPm\-ט3ZE'Ff'DTqR. &bN[hU[Щ%ihhJ"RL,BĜ .yv.m0#ј/=b 1Z+{R  &;}L0¢ɨro=zch7| z(I3ď\fo&#[M'AWpsbYjwG'xܤUtᤨ؎`)DjbWy&50WndefVA@0$vhH5V` 3HIc?#6rj d!\kfc-i'39}P(F*O`J=&iW,{$F|d=jl-g ~JW~ <~{R<Ȃ-0)b:KrKlNЂIR.R (% 6>pXGI3VRZKBl􊡲Ռpè/1_eݱ&gۇ.֣G=gcVz vS'B! 'ä!>Vp*CWiVZ| ͒;S&_6۷/h0Q(ݓW?퉕O| 6ЉvowOA0UFsn #?yv 1ްvۍo' =HAu8Sg)z:mjGў3#yR*) Q:)&GEDzɲHEyjŷTT3ܯx+ xF}L&[Hf鱓="xj1A{|S\"HwHH4+yqxnMVIӾ[3Ua>ꆤtU PEfXf^{R+i3j:S /fO(u{KQ-mbl0Ae^uΜwFYSي;ݥ؎f33~;S$>(Ok Ǥ`qzyM'0JS 7Bw,\. zu, /lq)DQEt[=zdœ mq[@4 ۈy_+mݱS-ۇRjBFxh]3x>:C@٫/ 29qe8$A 4&K ;p'&v>Ext_:ۺ_N1CՅ..aQBMi%[t+jfuMJ=6V翻ɷZ4LQܑt u?eeX}uնQeD`σ8WWtԙop}e( )Km-mlЩG0[€her6%PGK([MH 7-5aZNzH@B9F~  `/l7?2h~dUKbc &ni*/0kݳ4OTQq~ۋTP/R|TP@' T73V\ a=QW`/=G:6t;,u^i=z a_pmx^ޯfglܤhBekWqө!dV],Y@䁫&O/7VU_Ҹh˾0a /˾m;+gzy6aK<W">b'% `L@"Ȍ4 '~BŅz󗏣!|j0zz oQ l)B;%G" %ez_E@t!m7;wg,BI@풀1~<x(|u5>(yYr;)Qky;?*?P"|Oɵp(scbPpÊk NN=\l0Z;xHLIUr|Qc.G'C쏘.G;>"@F X]=#,e@'.ìeSkߟv8SLTn8Y\*<97u_a.6@34 2 0NW.*ϒv*W} Z܂{sYV1ǘҊ)[19ÉsS,n }~zѣ&?;| w}`v d)W%;z,28|(Oeʺ`J|3;P >40YQ>SLIbC83V1IO xKֲ|_5irK7̾[ut'o~51[DYZT^i{e2'Be̲*"O(*!*Hc둁ss~E68N*=e:&@ʸHƏgNJ bJca}CYcVH RR+RӱH b. $I_sb)ǮMӄˀq6ZLEe, &\mAH1I9_ Dd=g*EˌVzG=zl3 :Fz v3p8k5`cb)?2TT&¥ .N j~ F|ɒ h@Ry]'%C$x &Ic1/o|ωS{r^^Ff fff:$  Ɗk$^8ꙶלdln MLkߕF 6KfX2 `2NmK1 @GAҗ:L lN&זxuSNkXH\6]X!IYpY/j1% ?m #`a@c&rPABLc_*P p: NrTN, b6W&^!101J E VzЬ0E>/ uΦ XtE *!h=A˖؉I 7l9*H,[{O*ٯXh1SdƗ^?+3*cE))N*E#IC eT9>tZ0vq JѣG{coc?{cN-HL:Gc=.<%Zυ 1Y<<)o OֱP%P1'eSf -/:y@/HZg=(*BuqGyDQPu?঍ء8L@zo")dcXnQ.R@廒QKh% I$tT* @ɏ $9& 5 f)`1f4)b( }Tq^Fm`73)vVIbӣG=6 |glE>}Jo-@r,~ 0D*qd ՙO̹ŭ8~;4xT:tqC'U8-K/$Y'b)V]zV.tU!'k> J^9Ycm?qtrlu\DC=v_K$ rJUe&1 .q B˨C-% W%aif {2D.RmbQ]*:/ cahOZ~T9 .o"UuiI&b @k.Ar*%{ L6YFn; i`#|w&]6RȮB‘1 %\{134t1uLIh2>\-̠gYQ^^W!zj@q@!(as.5e.JB0+uޚ5#~NAvLC 5ƾb >ڋSSlByDw \T~>aVlp<:[>rzѣ&?'|~œszu   %!3ݓڑc]{-ʼnjҩSG#^yqdǠD_3=䊋!hZKxJu: ]cҳRPYҵ=;?sP&cE^0td砯@gr=1֯e*éMy }[O>v+>xqs_0E2d87E3Ķh {YE4) qUQ{hAeu"I%ДNHm$C/!5:+5`4HzH)dtn$֐wDќ +7TS^d6Ai@ݷtB-ꂾ+nu]YKtkS '!<& .H։*(ԅ+R˼ ۠ (a%gX$WxBlg^G?SRV3c{7nJ={kнcQpcN`Ρu]zѣoY~u>E 8ꭰ1`GgF+71$d#WD$c4/H/4[,v<)j/P,j0p$+jߙjD ~A"FxbvN Jc_ vuu'bܵgݷva4 cd5[EU*І5S'eiBK#5BKI:P8E> 32&5a+"6Yʊ= T%!Qܞ+O:HpW n쮑+dJ&FgQci H"g:Ar@[x/ e4`7&jS%ކD.Ǧ[/عvL CTkw[&=EpV&:=QbLҺFRF$'ߝ۬`Q2FI:Vl/;>ylg-oF\ҩe6A3JXep*m%GV|."׾Sgش5Ft:Yጦ13I.T:@s0\~aP$5q}h\TLkURR1Ȫ3ad 49bpqGۼ+!I:R5k;WzHM)`ĽbYT{kLi1c6PIPȆˆqA 4\B{%Jo+ڂCm)0+&j2K)7!1;ij[{h9Z  7p~R>Xzq3P 7o쑱|<_s}؜Mqϊ&{lnb2=~ڗG=zls܄zccq g$y^Uޕm7^3l ~tń%tRfs^scWQ*^ KFȦK69AI4-dm`QJ/α;Xb,w Ͻ}C>-{Z_bq2R`v\)(knnme{jcqtiQϳEri*L=SM/n?IrN }$΃Uu(vUJih!|jv9p!'WD(J&*% 8n)c}GISڦ\XQ[Ix;ky,@EçXDOh ik|wu9S;8sCs4YVR 1|hڳʳk,䙐!G>>|]/'nR&Pz.C_X|=O(j&s筚I0U7 D.;fku_较Bq$.$EYA1-XbMN5`Kzѣ&?;ozuP,np)=Ps>ez!fÈ7_cS%o%YM5ǒ`פ/=lJZУa2'{GgmqN 7&'o$^쫬D`v_57!|q݇.|K!hPhS)h7BL\?vM 7p1:u[R$w-nhG$'WERq0jख!g4F̰u( N;zl*a~Q5˿utkSwI26960l&79XyR*3~)-A qC"K*| ,n@lIˬԄ̤:e3E3&+%23* F?P: SE\o#,'|zrSLJqǬɴ(oU.iq.ԑ.Ԡ,6vBORɣd0 $#QZ井Z,D%ϱ _ RLG tJHrВ5&| 8|nkM>Nizѣ&?`79Ro9 3 HL C6oK8zn%n [ rIIeDe~٤)#ӫYBqy).Љ.>]Y#;%uFWWxSဏ7||˧[VbB;ݽV$*Y^tK2͆E ATFRz*m Ehb PP %n>"v% HV~L/2e$J ԕvnrnLhke Q>~Ut/Jaۮ0кB6s;{e3S=aAoLZ2flQ@B #ڄyI?&i mn4 `G@Uɂfa W(ϰvK4hRbTvR[2\3/0Rue< &g)w+$xtm2'Ěj_ ԙUqh5s喻&8rbԔ_3T~WU4H=z/8x<[3z7fᾕ^2<3OڸWq̀a0^0e緰ʋ Z3$IC82! !FM6KRd07,@c c0ٶ5e'P{2#%&g޲o^t-ݺ?94vHZ[C 8 %\#ehB FfÑPQBR>\m @4ð`CTT\-QDP}hd}<+RwϓZ4֔>ck!1Fd}( US}/ BE;&l+g7>7^+|h ' ^ Aj7Ia2uh%`ǧYcu?q?5j= ЙbqZOQb+YQ a0fM-:Faŧ@']߇[ iY'ZSͧ>?–|!Q%uzѣ&?|GW|Ϻ iza0 8`e;5$2= 5ڃHH b4iŵMl MIڌ?-hzӡـBRQ5C.}_)_}#d?qFRNdN 5'm(fN5E5%c^RGF!V?ԳCc2˨1>}u<4=V:C̕1R?m^b (|~ƞѣG23q:F?}z Vx )T`<\–e='AKKZ""#I<7DVLd ҍ/ 3r4ʗ7jJUZk_?ydzle܂myv'w_!SEZ5Q(Gex$'6 Oҟ‹,q-@bZIӛUGAKX}}y,i"`dJ,N^7& I`3bQv(n$e-k/11C[mLJ@zF5&]rL›%>yw #hzT)&.Ȁ۪5)5?F\+fons KOdvm .A´̒Bc}%%?'#'>D:]F=(`\{qa:Y8k>)ȷͿł)NeC%@'`n& 50l7MuU`ѣGM<}hG|:;/p!S;M(cij DII(NT ߱wLp)x^XfJOgѣGM~n?gzu;8=K.p r{]s 1΅\PSAw~ td 2j/M/<#')]I<='-.uVs_|oFw +W!|6\[Ƌ+|V~I6]ZwZQK,~Y$B8?݂SN1]hT6M&N:e$1]gԡߐ54ֹ70RePgiZM ̦YIPU2@0w}p̥* dj9}~~1R֭ ԍ˿:-(ǹˆ]H@#ؚe\ 9/NeID.Pg"h MBlUb<k摁=R3x_:9L3 ͼNJ1,pb s7? &m+b! "/Kɼ8|p [ўف}~B=zg O%Ա{~٨f~6 O-O d _þC3N, JoRT[-}2LVM]$ b-'|}כ$:UEN9 `PNٽxN 1[٩1I{1IT+QNY`aQEY?)G @QyīVGӸ\HN/!6fA0Meu8|$`XJcՕ<ߑ:T`\H dHk㾅eLPE;%.C}oSm(MݓGK6L^J3#r QHâp 1ɌpM$E/Q/Zs_Y DTaS}X]i؄i <] MEaJV\>Pq>kf̘?;wbdm&ۇcJ8͝9A>R&g+KuCSB,~f8Z>eͿ/n]‘kؾkN!vG&c{M.ENSf ff %P$%")!@Hb.>)E ,\Llf i)w2ې= A3Ӕ&tOҹ谓{#y }6稁pȁd/:%:74g.nsK&3G&IڑYi=z _pmx^0kov $|_^Žӆٌ7Tx{FR]/lVrhm^@NՂP `hZ]弮r KK _{[ 0ჭO?1(` Ҡd'nh .nTsid.DEbLfQ$ԉ.:r@X0+Y+1}I`'P|$Xyoo^<:yݶ;׳O|bO G}:eUro)@" Tu25G')cW~c4n$M] 8Kd)_WdR]o'KL 6 are~L č 1@u/=^jºLJ H&}4dvP3*R<[] B3`izO_cOQ\BMDV@8}GNN849'hrOKѣGM~np5{"lpBw 70  Iìh~=:T$aY`INd;f!4/L.޵g=ED4w  Ok;%HIȯtB4Sa[i`A%"&k،鐟y:Q,GdyD%lCG@:'K@…]l80fxUVƅK[6F0S*Q_V<ʏ48*?4`P6L0ݝV@M]58mLW\y% }.$H )Icߢ.k|.,8FchF(.BA K= t1c\a˶lkvZ>?=zcIԱڣ@ =-x}H$¥Mx,*y}p-l:] /|k3~e8Q{)3dF%˄S$Eh$MB53IlyRy FI^cllH$-6y;be&BI^x34A:F 2m]6-O W]viBe.|hZ׀ 6]We~]f1mHAL TIOD_54O"r84fkZ38h oVZyAiI\?p1Y{AF?K#F. !,=pxIpE*id5i6 wXwN直"Õ&S_+Al@ `;DzYQVEt{:"GHZ>$l {g 2#i,u#=zI>?wc;6z Pz'Tԅ1-))^'QZ 6,'afP>N= YI)nn:_ "D-$%4ī ij`b!RȗPWmǽX'cjG]qݞezM$ P Uv@O"eVCByimX }F,:V/tW1AI&fᵧ: TQ!C=>tg*Fy4[&iB )%Yux>ObmXod:c0gMm8?b:xmiED?vLk\jvd2li: ߍ>@%5?'F?^/,ūp b2^[^VX=zc#Yz=:=~C= hy28 /nMŅz'~eX·z/أE&{͸6ڄH0b1@ -MQ ^yоۼR-ĸ{e)$nX"J/:/1OyyzDr_i[wۊ߀<|9}lt<w %bE &!v+f|r# ?LhZ5nP @,QLjCuӕ9,b\, RpqWQiF<\ Fm "(/BJa:%@fլXO+α9ɓ}K;?&/KfZ"0{w GIn0=3Sh{[o=zch猟{ iuGwD$ye\cL~EMdw{_53wo.߈-V?vޕ߿%hcU 8E˽IB>Pm_'kPPzDHH*s xw]o^].%- ߋ$g(G15+F@O< <emy[<ُ^{vraeA:Oc!-yrH9YS =~yIDLB-EXlC ђdFp]"c8Ā7O8̲$./V &}!ilL]9O,!,S5e 4ÀT|x|!=H'뺯(>w)k{A;gؾs@5\{́ѣGwν u{``uGwϿ\\%Uksz]S5 w>4lyt{_kh`eN]:Ɯ볔VX۸ᶜ^HVKb$m3[J%<\mY戗/%Y.QM ]%[Lͨܧmd-fM [k'h< i;\)n2~R78):^-p(RS7Q 8jH#-kI┥L Zum"h>@ ȡoxuq _k(6+{Dxr<KLXDRsyFqO`ec0Mch{&"K$ѿ o{4hY ;?D|"ܺpG&{` ]G=zl"s{F<[^ף@c@2JfLI$YG<$"^땕?=߰e,U }>xtQI!6%SY8SRE9w,!s>W `1#A{h*bFlfDHj ek2͸iP0'| VVďn_+[o[~7"kМ, pJ قHT JPPE[įi,*O:|!eJwγI;1P J-Abi٦ZRl;p#D4. z;/^,ښjPuߑ~v@ʮa ['$SbSE&W_92'J[~a2Ip+{ѣ&?| Ks'{Pz<ϿHИв_fsD:_p֯y3onY//_9Xqv۾p)L')! kF9WQ89ߣk(i,3^bQJzq%uI8XNTyX=O 7( )&:=(ٛrz6WLxn[὏ZvOZ,Iz7 iB/'?H?i&5`? :VPJO(qd-Z0gt/W X2R b.h&mO!!$ ^\t{U^QA NG}iv ~ cFvRW̢:w=6p R>o,8g>_Դ9 *4u)É[fX:| aУG=663g]G:ԣ{I z3L^MȗMRf?xCvye·N/_~k0cCW߁v6㇖Q"R`m"M 9X8'XgS,JU33J3omВ;,_Ɨ\=@Ey 0mL 1Ioo^=I?-}CMH}N, WӦ} -+Rr@uYm?f  caG ͮ8'%PdfrѬGwض&2}Kgp?lA\1~߂2: vG.+6 [ӉE]V4{Ij_|@(=G`'ɲ@R؞'XW˗5D>I\J`)*#:柅%>.o7Χp.N^9 `A(rsS=p盪(" TPq3xh\?u=zIp#qroP1=@If$Z ewxZ=l<[ϼypx|#_>K" &:qG'Փ: ?*SJs2'cʮ$*#GN9YLΏA5x;xbh$əڍ;LHcFX% >KI0@]pJ,Ԁ7?1yGmͷunm;tgO0GJE*1MEz!Vi9RFۓ OΘ;/V(*_Z3wǵC2Rg0]iP|cE*9`hHmC| I|GF"iBű fV3l=:~?IISGR<>rmjۂ #,&AC< Y6nyz7au]u=zIϾ9smz=:=~Pm|4ʐUҬ F/|ۇ׮i^?>,̺Nfhy)ΫJ1@y ӣG=6 Yi0%;|[-أ@KgQɿT KɊ`P??<ﰫiՕ߿޵;o|f{V>QZl]4xP*%qb10]q^#-kEr6x^Iw%w$֯E1bBk4)TRA@mlG - #;|?]0d |BnDωVOSvm"gfϟr!g=_>;]~ba=4ơcF6`s #ԮՍ!mKSyAJQ(, Mo+4jhɎ!!q򻖩 ~`$ )䕯 쾔۾c3=5OOѣGM~ !mh˽{t@=z/P} ʲ(ޭ>pNA Xmᯞ8>ym_.K+  H܊H)z>>^Nez=QмkAah$j+3$יKj8*R9)yE<~c>@ҼݨT^7a/6xNs(|f޺+%h/AOa8NDⳍ)? _3! R,RSK…VYKW2BXC,d, -O-b0pœ%hj719T@y\4Zw@u)ব-tB8L't/1 FbF yBw * pl=3L&v0k fףG=69 -u/>H۰ ߓa0J *YdKRw]h8i^-Ͼ}+mu|1lzc.uՕI"V"T9+vĄFE<6w)WTAiI8$jE2a&uu {JIfsv ' oV}'ޣX{oq ޫOX?Y;2HJSt!}k$WT'}JKI p!H &P`-a(0_1 a#k{bAmb!j̨AA $}H t\&f(-=͞ʷC!d_ELN3t-U$9VĽCwnN1װӥc~ac))u:?3 i,%KG=zl=Zo{PzmSSr.^|k['aܯ#*e<:"IF0(mF"0>&cgX:۫wo3G_`5lTNىaxM\^M.@Yet2 ~1p`H'"ːS/8 Hϟ8OXH^zY@u1rE1ݳ1vޣlۍ]>8_][W|\{VƅK^5]\IϿѹ9LR@zcqt=u(RoP r b =cy~Bލ Db %(P+ߘ գP',ԧitM({ :`j{byHF)' P^Og7qِ,4/0`-A۠.WRLR0JJOk@;4ܝs,JxNr[sYѣGwOU;G:xg Wqmp~LS^|e\ 7>o649-'U%~hGIGM.5ҝ w-V$I9.Xj-p =tSZ˳#*h3)WXkI;mjv(N̛"^}סvuC42uk ~K+`vۏ&"V\[ޫ .ݼ˟?$ ҒF߅sԺQ`]#6T)<1iԲpr-,h(p1Y޳u"Hm0U2Qz>/6RY.y$&~|Z67ź;I@<@ƶuvкjŒaSȽ`/w+ ̶fC Yv ҶhA( zlm?2Sf ˱ӣG=6 ؉[G:7~av܍;utzgΜ'kX]gDeq\+aKk̐oN NP,;p']F|\NKW}E ڭ"u -5ǂEJW(<QBY*TT ~;8888)S`XYj@.^<AkD'y8h¯} ze6." ˄ٿT f˟&€ )WUaW@+!Sʑ-G)[>꾅~i1esJQ̈5䙈B?"J̅#cBR0eOYʞj ?díʐZtz#Ej9̷V;KE"BLa陇]"5YDP De[ a 2_J*{>gd9Rp͛/_IfڐfϪ$o@߷mB# rlp>?-D쓎 GaG 4tp#Z'~=\xzFG}Pm0 زe+~꒍@<)UUU|1ko3!gNg0wbBA21(LyBDhˎluR>".~y]lU⠊Y>jMh[a@uP|; 98+gM6fD<_` |(ĥ`HHkQhx pu',T, ]aP IE"73-pljCCxhP=ǰ́dFT} ê*-!ehi{rrq7HD1ҶDx<,I)9 ܇PF]< 51grs :Drه;c'G(ɳfiK{{Ȓ%Eˇ6|e *:sX p8/&oeBQKr?iY5HFD99D IrϋѾ4"l>?Ippppph1!`=G9 f?=x~p/8}⋏mhy+--F4EA\'t z2N9%<}Ġ\4dFheq"\ GxI2%|r;VI5nq %km󼦺&#@ؘt,& l~e_lNH߫սCj*h4fVli ԯU&#UL i359h Z%mi2HY? 0Uұ$/ ܦii X۷TXoTԦrF^xv,JH#h!sVV&[I2d{GL^ d)WfV;8888S`X _sp#:_a/=Dct ܹGểW'@ԳG1:sO hI8 m@pʋ#FiӿƟ}pǥdPfvA nh/wMĜs6KrKQ[Vd7oC}׀3Sgv[gT9 ]BFڛLEu`kpE0BA!ej2CXq͕ւzB"_ImL*+y֛)$n|w$8!ųGd$ "/ :\b"Ee(:QO K>GW@haXXşc &K)}3eNߧ<@tRa%W%Jl~3hD`jZ`0G1٦V/ B@/ hg)-e,IyM*eRIBRRKeM'IV 1kgT򥣱Qd~8^J6:5*U*dߝ0!"'m!Uӷ ꝟC ?0 N@jvcOC8rp<-LeoۆŲ퐛m[6p޹?܆+EXl:t(UW݄SF7߃nz p@&PKP@=gSH.!ƒ3%jִ|jKC>3;n2m se 6VG[<9LW++CƠX?̫NvG+械u@m7ԡzG[9w}0le<)_>3>d7d)+K#l-?>٦ ~>l6Zjx ZdE!sPX$4EĀFXHW bn:/Yc$xzĜ9qg'nD 7]rU /l3  (ΓGY A__ 迿-Ĕ^+nEg̊,<`'g%ߨ̀2yUFfvXe#1&o@d"Xv`2lCChoN ?CkP)VBNYἶؙm|Tt41'LAO}P, l~.O33e AX&yE`"{8,Il_t9hlST  {V[Wc3յE_o,*;Gr瀟L~=C' Жbsx1rK$k=&ʰdN+dςp -T~YYyL0@Zs:w"gxqizppppph=1*  `=G9h B\ry{;|G2><ۣW@ H֭U>%O-D !pdR&sn=K8y]5q@3lC 7G]6 #^]r`]WS PpfMb-D% sE86˙fn(0ٹHdQ%2%DЦ6!C<&CЋZ(pGa.@]dƦ/C+!\E@+vVT5?E?4붑 cI) dG8 f؄dBQy&+r^SaSݔI,qȔ25Pu8_2j+ERZv/vy93֛G *QPhvA~p {oEm"Jg'Y8 hA95?2Yʏ'lC 3D:tг9yю8mO 8&`!up#v)gt'}= Ro' n*lJ̝3f{_>yXnj ye|>s@2ee /1ZX\;ֵ3tذa#֯߈cOޤ'зo/w>HmڴK.9ƻ3= |Ǡ!ǶJ}:&_ ebfg)$w t(!RgkJ>킔peiӵCT}D6$435d i}MgdLa 6G> H f÷ԆiG"M d,PbJ=MvL Rɴa0;FL Dx+Dvw5[uEb(]lm=l;|pXBx@iC 'd2HA _ MD%gr>6 '*' X&(ŞiO CC'tX^Tv/ -xSP^R[| rmL"XHAN}efQ]G>AB+](3F Dމ@\Řކ>T/`u)[0R_J[AQpqT9ɒO҅Q8@>sq5 GrWi4J;AŸ1z(BBCC\_Lids~~~ ѽ{WH}ӧu݅ޙ5[@~k/c{ޭ=|x˃λN N7'?9zGtܹ 0bPW'6n?qG^r@g@/C쇾28d;hrD۶`X42O%=B 7P) "^rv/o$  }/AP$عAeL*e6iSYM$9Caj7F)&eP{rmA*>QbVVo4"h (A(HvE7jJTGkAGԆdeľ֙2 MH###@|!TiauF/ghABҮ8Ƨk?ݯC`K0 $|Ғ1_2+gySi% Hzc;_&m 4^?_K? s re-I8y[e獆ئPHrhiRH ;88888SpiU]zkq''Ź6zhs h׮-bҤpιWaMX,g=јyGݏmy^@cFs=6m-znKS~1Y&xqG+~s}ÇGG}]p{K]"OLb9ē2tt yoAպ~[$:aoumO:ОJz͉+Bau2 h_1CoeePObG҄Gk,4d,M>v?FLe!'m^V⒀. #!55xkVSI]VBE@kf2~Z}ի6%GGҋ#G*…_cuYAD[ťX,P%xйs9̙x2[1#CRt(+EϞQPm=|LO:O=A\!@$;7W#9sW ,f\y卸޿ t|t,q}- ng`G26deD$l,bWбPurHx, XzI~H&A(fWHtxAFɳy[z‰3O#Zbd)NջEf U#TYDmRJAQS?4d_}%o-naL,]J k"){/%yi6>8X'A6_eLRN}YN_km,ڞU9#0)?ZI0d/}+.3N EN'L 9x6U[2B'@J(DJB~-y@1M2JfDo)q3ϜnC`GgD78"~"F@v$|Ktp#vyk5MX~:l~=_E~>IX wxcpm6hO1]_2sbV44 7xE#dHC4ɨ/] mpGA~~>ƌ=ט"//q$6l ½\ /9^~e<~|o%f|/crZL#2 ZVȞIdKE^($-A~OzHcuU/@E5%Hk_ ؙA0f?kRTF`(( \6bda%xa3=3"_oJح} D޺X5զx%ѵ- ^> ᇌdT͛Z!Ԥ:L)tKهv/RyAf 2N&g_aQpttOoH`!LNvҞ giHfY%d{Auf(;(R ͸l!d _MS( j?xv,+#)qSRU[֡2oG,Wr @qpppph!g7c!Lglu ]*) ]tN#e &|TK1`Ƞ ԩ[}ޜ8C~55-?oד癧{C3@fGhSTƁYW`Ad}݂~+Ծ]>::t(klߘ<7p%2@,b4|`(Q f@6|ƶ`m(lvk}ҋ^a6W%-Fer=;s?쏉_[VaGcuKN_&L:pLSpMW ֭ǡcN&3oд@}[:h@LHj|0r6v\Kٳ;dLdQc) oO a{YٌKH&mᨮ g ,7VLy/ ƀ3W࡯|D =C2_lm _N꺜KxTBHS}Ύ}1Š2%Y-`%3ta8 d0Y=4##tN )B,#L64'Z1B9 oF_%8B~ܠRpk;T~[mQ"m? v,,?zG--l_SSV@4a1hqOZ lYn"H>Rj%+ b>tAYKA g"f i9?P<[L/jme<\ LϏ1ɐ N s C|˻'"hRu` }hs";ЭoXtI88888#j `S@͉)S>űǝg #8Єn^W0̜ѵk4aܸ˦ ҅yI\xѝ+n!Oq='oy4;,o'x0k =BHIy< @ 7abLLFdtĽrT^h~Ua{x5 B9(JG)[> C+ cJDC H,lN}g(: #$O%9fbڟ,V!$Q\c&$˰d^)Li !@W㓍TDb-qےk! HE3OBMXRAT Ҫ l\7P]]#АBP< OO qϽ@OO<4 >*ƥ}ڵmFiL@VZa{Yvz(K/>w+L}& _~.N:X,Y ?ylۖ30KPLr2^ ː1C3W!fxKF/7T ?෋\1զyXf >pZM 52 DwE謶a*t71odJwt$L}UBT* Sv 9M\=Hou|~ zgc@z#{+ZHmMu͙?d9Hb))DQenB >h@4@1>J26 H5g AI>: !b/ϐ~0AZ̏Yږ"9 w}YX`A-X`hPi;8888a#`=G 'x_q7?}8Єif`~?C6sq ~oEw \ٸdx xY?{+N8Z=2eL6I\rN9ǍP7oE_cEy^#xm/2pSkgE҆nX  Cil~ BK% Vw2Uu<Ri`V. G I@[`i].EdD1GYQ(wD^Oގ Cl3wbA"C?.18k}cUUJ碕ӱ*C(н@گR۪Z9ފ©AA܇R[lR]2'NAGYx=h79;LO0Ya&CH^$Sk0Vْ UD&~[}=LJ6!F5k*IyE, IhVD$g'e Ro$/T-ѱkЈЁuOi`M\988h#?5b(bb("0>|o2du:[e]A [ ~~xO$6~[8_ ?vM~ KfiЩZi(<'~@9q@[#%8)CPb1{fo?K`g28 į E4lX & }ʹA|l?df?(ܤ*MΙ/t`Hħ0  3dw6#D(%{' bq?=Kmh{}7,GϏ:N4s~ -@8p vi|W% c=? 6O>y999hy_G{}2jKZ٠#GQư'6%wy}3@ƄG0jHpxws`@FYTQN`*LEE; j؃,)@SX|c5őOUےmޘط|frM vB('#:ͶTܸYa^\֊1r}hVK͂d@@A⚼u{Uh%x\wPaʹh-hzC쳆1%QȞǺ9%t}ֿ$Qj{*? H"i_,b\ DB F<,/y?ܗNLeie-@TAۄm s S}EJd&BھcPP5ʘc|yrfϳݨL!F"MQ9lt\74pPY7Zf^u>?-@xc'.L0 @AG ݆xR rT =}͉+VGCf *+{ \p9'<-@ tw_`:u:}xבLi@c lg<)HN@QĎ+@r@PGәd*-Bt!]g.={MoϘHIXБS.>2-{1)Hgjbu)&oaNeVIm< Uo[m52! RbO$ZN j`tdt"o+߶ =H#]&#-&{!bɓN@ ^{%{A8ڽ!JȳOLWQӌ\gTc?&O7Q OR17Ze3#CyaO8,K` Zi` ~-&j@Wرd0f}K4ēOŋỠ|_s| {D. <#[ }Z @.쳎Ǹq1t cVo3`T-4l \fg@A2}_Kr=r@:=* -\b5 ^mr♉xp,Ft_dfN5[|IB_XP gPYi[9ųwD1)-D.c"7Seh% 7?nS |=Cu  r(dq/@H?n2eۣ]@.+3ġ϶9&Yz$xv| %w@} f-rv_#(droyed`[*~^Mpz2I$CbxsD YUfP)88888?}`GHtpp<t1as/:X1vh4-=Ƥw~&W=lqI⩧jW: c=hkK@}/F䘲A#pF,gBP &c1%JrP!D&, ~jH!SKZd)8} ~>VnM0eA ,̉AAPJD=/lڃQ P h xT୬E]3TmVhx z7mU!,DkA{;>m8d!<%( JnyPpz>8ˠЖ`OeЦ7fP]1/ gx| ylzI?߆gS0e,7e3^SVbH!~<忤e7@\z, VfV-kKr*S8hF|} S*:~IТ)_GLe_5>Gwm}I h$ tSIQA<(*Mߙ #D=C+˔*&Snڦ>&V RB^ ^$ǽoZy#QQ듕S{]˺_[2< P9j~6H m? DG67-0Y34C)Hǎle9>%CƲemB #(ccLEU 9vTnW=ǕL{i̡eJ:{!0J;Eyʲ5} &~O`X ` :p<8Wxw!7>wqC^K/=M:u:;4dwV+@2茉Cii p7{=(!!Ƞ}xp0LӦh@HfDm~by>NJaHlOS5ZWtDhO%R=b!=y0B)N1E{ePYE&|K7B҂;>]3ڊ/$PFI>#2iuTB-6F!\XPv6h_zZB{}_޿ 'o(H$cyv*#9iόo Bfhfn?t My2<8A L&U`,cL0B];m#eHmCsYR)y*BD"r"el()ب3FT*!׏3bE!'w1Pro lL3~! ` ݻ S|G +<&lڴ{1+WW9q޹?܆&X =zl1'>{2$+Oō7\&<3pr@#,gOqjWk thޖG{Nh%r}~r:ro&;9t2ESQt&J8 Q02qIQs@\  J}en7}R 1# EyRڄ>yyyh©^|A1=ګO@&Ds%8t0T$shHm ֆȍz˷B ˽_dUh|<<{~Qtu3S}VC-h!-H~=TҕTJ0z5X A, Ye F +@V~ʼޫ7VjꊕF-pw ]ͨ7 뗠 o- I*(3F!f<Hy|_.kh%.R"'X N94OC #ڗ:NYda17etR5-C@T}įKBA({#mlƬT}ǖr :m٣=F7ߴ̦K%".1*V< >q<{ܙUI~F1l;7wUtZ!Ӈ]xޑk{3'Ѱ,0zSmm(g62nrVA&Hy ѡK\t`|~hķPкH`dDt0@7ZoX@?Yd2QO} +#;gE8+1ùYetO 1 hڴ((:薛.}0EDHK/ޅc&yExwv 3O_ [܅({uO!Gl^2Z[=[۴EzYFkaF%G3N=:'ֲކR mfu_!A΢25vۑsčIeO`1FNؑZc'"o&JlWk/jBd5fPĽHTC߼$Px^44/yKM泭2Ht_0xᡲ)>qiGy<1b޲˼|I1{@"eڳ4Y@* \/6nJfm2 K\;öCkms~g'ehz6ٷ?<ڳLjd tRZ#n}>?" mj]a'"{ DO)vn$7C:#! 37.988@ ;? N>^6m?۞n9眀;O?}G 8Spb(+ݎ;99_(,,/#yǀX,%~ф3 V@ !- #iӑi!7A`m%]3X#|<"J O_.2ePU,FЖĞe3t jq>{9g2Yr_kxl55 hR] (oSt.&D\ R7 ̍@h7pDښҹhx|N ?^nj͍e{Å`@[jg~!Gla6Y0KDle\$'' ZlA''$lp F33JA*HB ++ZW!} Ĕ1#L21[ ׵Cy]`gL΀&HF.P98p3?Aq>?3@4ٓD ,໻G^F's٨rHe_{cDoۆa{E qǦMxR,\5֬^eܩйs9rs)yu ݻǻ＀u)Xklٲ{@}ѧwOSfC=O<NmX^Hx3gƊ+l*,Y Щ**:BN]n)van6 9k2 D>XIg*K .!=C$x9Crv<@Te]Bs7 V`A}]ly._=`A {W=PD|e!5d z[N[3fNjvK@/Ieڐ\NuPB Ҧhr@)->i17ⱜN%`Pf0/Cd21 HfkǽldO4ʐ1E3?qq"Q4 X_OBe9{BȌ7 ;;C~7bU.P&!;Y^nCuZ+X]ʄ-ĸẖ`e ]dE2#Q~C#h[ /;BaqF#wB d"v#xWurP3@G'2}]p;$C | bGq& vVCe$=~3/⬟߈^l6jNO2rv/2N9vm !a=`gԂ 96 7*͠_@d#dK&u@ b <K*5 B\0?~Dy 2dAwWfgjvc $!c# eYf:B:4nvF@2Rudu-QBnJPN{[Ѱ~1NFn ZZ!-ؘSXS!  j3b%%~.ך˲!%y9چfگ2S1Yv2VO }7hZkx9{'g/yF$!9>Q~?`$I BAG`WhX2yI㟱-9<.&@Cھ(KByBa/[b@>ii:7Z9|O1noץ6*q؅Q&~z#6f'~w Bqo2kvx(֭p)q_/~%TA3n8#Gquw^;vt3@$vi䓏>P( Xj5}YsӨKÐ "&8#H`82u ֐×Y Eԟ}`Ͱ(oqU~ ?b9B]#K(i5,M Ԯ(U!]IQ3 fe2yTH$m?]¥a̜?\D-vCol:@"_ EPsO VDfMFÆ-ѡ¶k󊇶 y  ܈ B5{)'~c#)0'F$S6`Bl6^#SR!ΛqXaRA&.zmUg&;D }u0H#:q^vr5[g&5gzB= SD*;:d-m(Cqp"cE+mnI"#~:)Vv "}]F=rJKyLs2rSԗ>9k9 N8~?6&y1 @?:j_1l|#֧'wX$ܚ5kr\ǿ{cQ A!6l؄eVW&_ D~? #V׀/eS;xp]N1?L"Nd#mDb$xvu]'F,V"~ O\|'ԥ=Q= y`ZW}r.ʒ c4?xJ4vFDg gs137hjB1K< Ey1|ֻJB'脥C T)C9VuHkK؄/_ &t~n)] ET& HrmCfqݴ@Y'"vWS_-h(w"EnnnhQczB^m6'kM=LƉ=ԁwa 3q+r6fV&F>s+acYhXgRv-Ĕ#g$M!X2m$n'/ f' ~hI@F D#d p;8ecڑOξ>&~܀JO;@H" `lvᰓ;: xWur.#mNP@,MKMcTg-JҬaֈe JR@µﰏXNX[]GʺBZ8a\ 6=2L*P@*GH0bGD#;k,򀒇]vQMqoIf^b!,PɚHޫO%>‹N߸[罇dƖIFuZf@,zVhK oD dZ3QM$EKʎHH $o(˫)~,˲ֹ.wUiLWw P=C2#h$A~D]]]wuVf6x܈qH{;}Ob\cEGQwxe\!™Iq?$uH_@!y=}/!{plA"ۏ#ECJRuq]-}]<Йߕ٨@#Pq>?0¥ۈen_Ec W-q<`0 !#"dOW@œm\'戡Y6vDDbDH.R(J bA s#^=,tvj,NMkH,dQIl~$Rm6qMʭm%ld9Dn!d$lN?w&s;奪eÛ#G3KeWV}~Z7>f :;:@;gefp ?RpM}& 4 R_*$g)/$7]Qܩ$DeQF$$%+G|YU΅6:#m)~R̈4qV= |M<e ޭ!ڑU)1!P`}%x^=B< E]? +Cj0dPS!Lh1T>y(22DShI*G"1f9&٬Bl)Y)E%DB)mdTu .~qǛS ~[\<'(|TJPɸ 7 @\e>z}G@r;Y)Y.6'GjY{wo MA<[vU* lT?rfL>t$ji2i''wc`P +y{?{xYڡw :>gq˅*Q3t )aYh*,o'PC'+SpL(;Xr8vRuğWXе˰O L^m"@v^-37l)0bx,|@F O/`F{zVvT<#BA3xCfxɯ"dZDRʼxv'K*.SZVmv$& DL/eb>`yU>%D}b߫<&?@.gKsI-?:* M؋:V…_ k7[Y5nF@y%ꕕAď>^*:>ُT:/%"PDR{&XJ/ED>;RҖFhI'ASq?F!w-U:c|g 4)HDxuEjy8N0(  QdgF+= ,$wX( ݹ5[?42`x* Cd%=8y+VktGq%)KBVpRCM0X^%LxFiRaW,1]J^B4^K!2cp0/:c "&-Q/Tp˲,'N z0{hEKxhnneVI|><~%p?ȯ/89QE}iQ-hXRO{D7C۹誅0 J>=ˈ?б Fd00:5cץ?|f&<%b D AZpHo6%CP \]z͆Phj{} %zT$H0WCĜrP, J`6UC+=#Ky;]Zr%l#}+LVh~:>Ϧ\o;٫TOA&UGåtU\!17TO ק5 Eʈ>TˊT!O:pLj 'Ju=͛$1!ϒ%pmL=M3tIW85 6Ki~m՚cg*p_\ā`Ϗ% P3C @xc: # G IRCJ2xM6MQBHI:Vf *=*$7BQ)ϡt4I?\<\Z|yYqxK>tN<ߏmNUtZ8!diGM1J!J5P?cN0I8"6Ji1Yl&F i}[`ὟY>?K5{n/J+Y*/TNM*dmRA=]&+r9!<x cHWhyyiA rUD]"qV1Go9N#0$A0F8zmkHS#o*!YB 4#[蚅 `{sp|l32`x &$I\t5O8)&STZGB*I~E1b,[iWB(>o.\ZUn ?:N;>AJJ:F/3~ɞ~&Dm'#Trlk}6m{1ĞR#=UZ pl|SOR | ÇKx]s/s 6ұɛCg&R*ذ`:/%wu}A"#^Aa',gK֎i.ZQT1%UMG޺!&b%E Uغ|B1⸬%.H τ ?!v,hz)`.BkH͝KJ5喔NI׸B}~2 p4W|T??yWG۸d0d@Gg;tcc mV3qZ @ q`܊ xt;Y6sJAI.V!NV:eXRJT1ܘ,YҟBZ:}fgߡ".\ iVɳՓv\K[)bsD02}"$ IҺYZjװ HCy싨:GYgᅧ+Jhj3OV s'ң`Ī' z:VD> "tblO`5FS6˦2+#BiĈxͱXP Cx<6HXZH}RT]TW$;ŠG;=]HYI2FXM{HT7Z4i| _Y)-o k/X Fd0l!M>V xwF6t eJ:h~9 @J wÁ@1{߆lyӒ1"M*|es]QDRv]ĝ&axzjES &AKxƃ&󔞫oBȷ1z? ~NԚv@M@y|*cf4/Ʀ,Vgh껧q%]FiHmH"Yڑ^q8˿ĄIuN)1'U]7EI44^6h7%D?)3Lqg}尺E3K{&ѽk:'T`b[It q+mq!3oi_hZ($ަQ [: # -GLS$LC T(A&9'"Ӏ pG$hݥ >Ω*7J+_:\kw@C?=|r0NtI4AK eY)>`xM0ĻY =l.w$|;k-,\׏b2< xJ,3o}B:2z>xԪD$2*Czt ]$X=L"L.Gm,ςx RCH^mK )DWc" ԸG$!rFQpbN@K@gPb\vD3<^Gr| u& z[+Opȟ1Ɩ `BH  N^We0d`!J &Jl).Fa8%F Yr=)A%!Ր/W$C$1k$B(\&NlZ4rWM^kQQ7zONωƅ:1dF쑗?*DbAJJx*I.R 1R-K8ٖ|Q{[iseyT H=eԶƃ`_y-Qm0\t;ζՙal2cӘ8p.-:K d^hÑ:@|1-)D ON6VJqٯ㱑7 Z<y%Te` 5#R `TO$OTs4i}nR X UU F9i"oʬAcKǃ{/1pt#e|{/k]Sgf,[- #I_@1ES F2;z>߹hRT%N@j'X(i!~ .;uI((RZE3|P6F|}5p"\ҏNGoy'=btY&@~ʇڇ6T ˂2Rv̊. 4C@*yl*{VJ#SW"`3GeljO@ʟ_Bo]sqP|ξ^QG|cp[DF 4Q# D{KT4Rw{pEiCq.Tc@J{Z :ỈbINm0;L;0RL -Rq9i2W$/Nv)hFچH;,x?فb[ # F Ȏ9U927ug8'1;{\yf}P/EVK9 Z:W}<˒Z#B X.nU*?Drc<0ޡy}f\eThSl60cKCqDH;]CB=H-[ @)p'1r H5t^{/p rc mI ?x:uqQ=OL+:x$#ZR/2+V@"L"Z]#:¾?<^^Q@>ϔMio?vꈧLtm1EG}4M"DiAe_H g-, =| xBg0d0`X?fh^jpBx"h854V$3tō| )I^>H)F 8IZ`n+M;Q,NNڮ¦.aZ L|?? &6 iTq14{\#!p嗞~a 9)=x z÷#dFA{MY+(K/cd=qLPs3t'P餽 é<"c l E L$8|Z$WBʟ8Zl ' Ce_Gʐ FYk`xMs7;8J(0?e=Q1e4A̭yv3+Ww  LAmY1\蟒R3>IB?xMP~HY6ӡ'/XIJDKv޹su@6\'' .CwJTS5J0(~ ރ_NW fr?Puz?6 GE>6.M=A/T1us`tWqܫQ:6fD"LUcxG3%'?A^LcT!N#(%3y(A$ʑiCv3:G( ]A b!dDlJM8pG;NO21[$O "ٶoy;~'A36ħ\~AGɳ H)F!.|V .Һ K,Tgb/Fby,^xfO=FZTBwyY&<o52v*k$Pf*L  O`2abk -@@=qUWJ<2fp=H?vLAb$GL%0y=>Dc&u'F AT5q8|5iBG"+>?H5<`П/, v>?{Q sK:`ha|p qlgj I:$u)B760/#N?Q2qPd3"|˶$_~Ey( H{\ڔ({Uy!V 1l'Na 13B0*G)($9'YnX ;<0)(s>TžCu+\F(ԩMcxȟ#EP>W޴q`00?( wn? %|Sd1DcT,a,DuB)ɕv$ĹNL(M6% ka5>SQ0^f#$/Bġdr,O[_x-f TM{g&_~i5 1ty+}@x]HQ*6i&]yT@si 6Icϡ ytp\=BR$ / m3 2p `~S < ZWYʔ\ff@ ޻pBn}^3FAqYZ̭4$*&Kl'DʋWfϺoWv?6֔}Ma%+X{9 s AM$tʣ3}LB >ɛX7&)MBe8%&Em|͛W@J!2ҵ/m*H8}UzU$1uGĸ(shJf,jt^d]QW\49|k"s)--cdRQb5RJFfzC~̋PKjC I<@isz_'y+ "T!8oSrKRǒ+'G?Y @f!x_Nh %.J̣V_x$+*ãwʌ_-&ImR8C˞D?J#̣W,C4H7iZIg4˺t-ۋ|p;gaps|bNfQ>u+#ou"jġQ x>U @{ΈϺ۽xDQNzH[F |^;>?;)p@etSpI U2gxATGV}zc@cz/}1 r * Z yƒ! Wcb| +Fɘ3*߉LD!c$NpR%_0Q种_l%L#bYpeI),}oGk(WRt?|%0<k0`p_P5,Gq`0d0 83;ځ$@Ĉz.1,eu iD^DΤBDKD=MxNʐ@`CeY_#706 ɲQIl%Q`b7_.Tr$ ;ݧ_=Fu/cav5@[V 6_@g/#skO@Z޸65>N{s֒@jO<ٷŭ|sCM-ݛ.3eGEIp[u%+|'?xp^QI$dL:SJҕ*4 AbI7IT SJ)&%ʲk_*@V=ƉA$@x"2!':{w8|JĻ'Jܧ~g!! :iagrVPۜY A_uPeەD4@^2 YUrŭQ~@qJd2$/G i|v"9F)ƦmZj'1nr=JeFv 9.t2;B'tB=O] 8&tlDRc%<uʙ !$w7? E˜cSGB{cŏʙ {Vz/|(bb$)k}8} `3< E° Zy:^8E`zG\ͫ%4[41Lh=O_" |QjeȐDTH-FKzAɒ/e}|CO77K7 M@ة#*j *$*{%rZ6^3,~%Ǹ -NNg+J%O X9'i`_VzS|tmLAN O^KPI7فN=t@Kz Kø92԰P H$'PDqߢxԻ/n AHyKH%xswAcJA>0\wd2XcЯX,y"*(H,S%̬ro4^F'I|&%"԰# yC%8ܒ2~O &8rJ̯'o$<9`x }g0|S,  ,B1 EK>`` 0H8 ,Q%&R Cc-+I^< 5zN$0SN)T$ ٟ7<R:^g$k'e^fps*\K򽓹,r@ꧪtlG-HE^*AR:~0 Y8q?1+%?[:HEG7qR:Ց'OY|IК^܉+[',@P֞=t0GHUH@H)%@ + ߜ 6dAqd9XE<P,'}tOC8ds;v<)w+r(` 7,/ {!Bd_ I} &gj[ޭ(ˉeoxD'E~oa0];Q W<`0d0R!˜̀s=FHJ+FHÚWU+g؆'ĺ` (Z]IZMѲ;|'FEɢƋHBȭ|{";{}tR}Iwu.F77w^@5&~~A#jz٣!%܇an9ۃ-m ;8<:Sp|}~t_>[7[;zcԥwjqApAU9/G'%TuԮg9*ߏ'Q"vTZgJ"舏( CH!+%t s([?zZ}Wy~) B(NCMRBqvTJ ;E0L hMI%bA'`kIhSL Hl98M $PݯbּߖOнgN <-zdBݯܷq~& WʑxT\B"5ywa [eteﰙHԧDZ>?zw}/*(Dbѵ>yOV.ǀ .8)$L$srGgư"dEpL5J:r!o/'~b|X$ E~e%`n # ÃRa"@@𶤴6J%G9h̑_gYK/DND\p2+IǷ<I6Ώt5zy_kLlz%peDη]?]z_hAzG%}ֻE~P/t򫷞NZ8*CJHE;6“ǣ2赖p< wQZwϣ@Ojcm?R))ʋ$̖HYGT,#gS({YI}V*EaV䑐AGO9 'b c?Nr E~5!"fGixãϭ|J֍3hPT ;>C-z^PSa&I']Q#}$JNO#:;Ȳ3LQ$Q5hZ׭sd=MK=_w9szV*dUX=tx(>P;"y(X '}65FqjT5LĐ 2n w[7-b%/E"φP ^ H}`?{~84T@ض>@"J{q*h/ Qp*?.HJziOJqw=dm'ˎAb'fڋT;NfgË2my!^5Qr\k/&>uSw@y) Z3 KDQz}Tv$<2_ YnuޥJdHZ@i-E} Ӽuy]lzњ=m{gw/O x(e!m!˾9(p.ŇzZ:6XCNn 2 s(8< 7RoM:"Ŭ!|}G/^0ìchT@tQ)?QE} JAqP+/ֳ놭+ %~٢a许/Y4t4|xWTG^2@aEAb ! vc ՠDH"M-J#xM+7ݎ@AZrjyYY쀭{sCIEeQڃC"g GE%ŸTd) . G^˼ 'x6;gG9|Q^.hQy3|gA7;}iUp oǔ/D9̟v{'dVg^7{,*CS|||~χT@\񑚏QҟP>Oyms~Li&iE=Ec,ZH2y .c@2^lەb:*Ւr['0g>?V ﳧ>~aԢa.YsC+X '~c`xC`($~i3|C=yPo4QmhV84JG@n'.sI;hy:ZkR8qB: Py8&0bG#ֲ@wv ݧ_=3}WFz"9٤tD 8?nOIiYZlάBODVV;|~Vn{zܺ-O]==qiGU5eRD29= R6o_`"!%K刕sB:k XDm3.M1v$ĴIJYhA($4ՏJNҚ7EYC ah?ַqm@~v BZk7z+[D EчN~b*x {-߲(` PslEFXBcRSr^?.cL[GWz@p< 1ŌɣCsh-B,1 l4ëҏNǝz+;2hP/SeWs 0J~<( "InigY+uW^6|'c7SgӴVaȵjA|_(<<H%BChߓ\I;AZD!02B$64.a}hLZXG$l?Ϗ*]<ݲ/$8z*\P Y` ~{WYD EK޹}F?roP !֓ ) V(c umIZ^BcRrep[3gx.?\v8OBe^.[O>qbFy7>UT). s YC@1߽ۮT~|Dcp lл9FA@#g@¥Dk!(9.`G)N.oƒ,P(.YUӚ@秙Fg]t.xqMg?-*;gryr 5gv!NL#hSؚAVxbٸG*d"HKx լbA< $!(@(,"c"=^";qHcvyN%[GZJ)ZPxϏNy %!V\\Y-IL(9?pz`0`0;P/486vVmw\[B̬VB&D0%2]Qxč%P:7.iʿi*,~MiG(A?`QM{sWn<,vw|-+H?t/ &ŘR>Au˵cKxq_w$(1yC;‡}d\ KSX1\g_W>EDSoQE'+B+d]H113#Kz4_(W:ԶQ#W֫`6t7..xsEC;I/Bm) E]?* ``0~8+P Y,G']M"{aG*Y" +:0MP( LVwp ^,#iG/ 6uH'X[JeOwޝ}uQontywzr׸Q-ܧVQ@b).K͕">Vު?Pߺu O&ňQUqc'%I)E\.?_(Wlr_ "tzIRf\j'tgv䂉Hu#IZ ,]AJ—z~/<ÿwpHriP))S;v f NQBdCAqWP1\5AA UWjp[z}~zM~ !}r&gPՏ14HZj4@+.`Np@yH].]cE$_mx,Dh,ʐI 52HUq 8iC!<$?5a^>=qId5}Րjި+XO~. &'/>ZIiO[g؅b yW oQ0 hdYs6JEԆp 'i$;xԄ 'CGPI#$RN4P’fFGYy~vx+Zz<3]%C b*  ެT^vn)~V%9)/H?"pXWLJ[$B$˹I =;0ă{ǐ|C{KsJefE90#>CNuc`E!^!{X~tx^{fKڑ T˂e(ԣ icAgI|X,Χ 8>pbM,I/tO=;{X.@K $L|PB̰,FؤdZaE(V8Bii; AR$]}WO_YUVg?ս=3=yWU7?r-1'Ig_ `%E"ӽ/Rpp2>׻@ @w3n?k / GgBa)Vznɉ"0B]=r!"vCAFx?Ia܄`\:7]Ƶ7`cJmVnYl`"uy9tm2D/ۅ Dd~KVN f ,m_FD+Y A!M&E|W @͙:6!Y WހGĦ~#9=%:mIw ~Co2h4oJêk֏Kq񁕍d^F/<.MAa>HI\ -#vL;#F3t;`))[+iѽ]ÕpQBb($`ğDO |@?9oQ_,M b pp%р7+VI >\[7 3v*dpr972Ӟ;ӽ]a)qVx'4?/~sQ1T@~~b*9cm|}4Kca5u7ްKEwf>Pp!L/g+G$* xjrkN)~ @J[>g mWaJ?\"&ʏrWPOZ@5e(m}-3.1Q JdyǏ{!,=|AW)oUldeCg^sZ-`%඄D-$L?-z>PԾ =r~t/r~y7H@m0ټښ}c"V_{vgyAN k+\8܃f=S2!(pM}%όbs~$OQjՕoq:x8 r:F'@X)օ"JQgQ* Ge;7KD4ݍڣ>H09;7Զucr~Bv p3 "^FA `n$$.~v Q ) A [L!iݠ+Dq)Zd\r |Cm+K#woFl ﮄ6Cmy \b&t#SZnKPlwoÒNMhjSݵ̈́ {g:ڟ\|X.:KR:0CuVbGӅxyyFj- BjNԡ":Ǩ.F6&jcQg'r-\-7', HsC. T5~ř!~# l=y-`M1,@}GHjqEFK϶kF #сMw/cHP2֑:/"~Z>|C`cN#GAa6忸KPL(pxmH11k.+y-\ߝ<ݑøi;WS. GܟN刟[N_xL 򝨀O]͂V> LrD?RN 8!Wj&iQi<-;ZMa'n{O;PY8~kU?r~K{vI!HdNR[ei^vx :@v`FtfV]l[XErGXRnQȴIᇞ{`})Xh1m=>Z/iO>/kPk|].OE8r_%)' ?o[)YK/B<(A.1R;DOq?? |3! A A a 6B!E>ʟ|KDTf0lS #;gu}.rmY~Azvҧt5!!KU_OX5: nDduHeI9E}Wi*t?38q W>@g,;9h}u )W)eۺiFtPkfu滝C²t'Y| mB*=̱С:~;ȔqލJB^1˙`uyED﮴?\M: ؾfq&`ar|@ѵhyeJ П1x J DMkX(h-3(Ąm?\ϝ7gq *sخ-]>R.5Xlh5GuvH rbo&|B,NJ'UӠx]Af^-f1 EB3|YTfH8BxA p*WPG݅h! 0ϵ*t1<{? ? ~Bwh쇀7{8BD Ic IM, .{s9-:v>ۦiaupƓĦiϕ!u r,%>5恗}|O6'P@t#>6\%ێ pP7TV"эn/^͂,Q>$J45ZH+*V .c Thdz~9q.)"[ʱ@I,N>3] X)͕[ދGfbR&ܽ½Rh/Cohzeho}}e;ğbmkScGDLJEaD9 S?בv/xdT?Jk긵2ߠ >j|t'#藃X8 `s;ܸ]DbzftSkEnhgA3:fcscM&6`x쪐cnaJ,GjY#k 먀Q;5=/}9($]JBJ Ӊ@$@!(abNT)lp$H5%{blr{摩(j6p! ?hCB}z9j!.,;R,zx?-1ד웿5IeMiu4]cTus]aΟqS,TE9,@ @6skopw<Y\G.#ɕ?(sE%/٦! 8mr~.E (=LB ƘcayMLMbbeE c`F2@iau8Bݙ \-4qn;<߀h,$* d[nU̻_ GW @V)[ rM4c]]A. oVnd-gW) -dʣ&n&nU}hm+~Zؚ{X&)N(hđ}WsAU|\q=>CRC[l]Bqxϣ\sGX@r}/ƶcb78Ԇ+!'KsQ}IT6@ɼQ2Fס+BJ@Jefޏ9+F44Qp<@ pa<b3܂c]$Bryv@!Zghpk0W7gx˽t\?f CԎf#L`mlf;6 :ח'g<*ײyT@';wG$AaArH,+YH#Hjs "(MD14ҵ0\yZ{SJHѫ_kar2W>!iZù}hfo =?!b!IM!$; $Pfx%JH4oX#}XY|8xOFR])t'Xm@O|F+1X|Z; |;K@ `G(UD AP34[@q>o)B3̠ s¸qh@֖pA1:.o[2'{eNHqE!nCfL`qA B|OTM_ʞ\vr Х9L $ģQ̀A|,j!+-11v|xW~9?<1w9yrqH\o61\#c;cD/:@8Dxc{FpB~a.ͦl6Ғ$‘AMZ>In@)W 7^pgm{G٘)L _í~&ۃJ_} `)m.~2!p$ie! 2mo֋B]_I?`xX @ F> !faw\4Z!KW)Oҳ%>0 @p34pI<)Mhcsۆg W|c ^ m&3Y\!T)go~|jWs/9!"Օ1TR>,@Zʢ[ M`bn6*a*}x7]#ͥ'Q'1OQS9bۈ)yո`r~ΆOE (v8 @8yFfL?N"'i4rG\@G.9b9FR|ٓXO@` @Q  ݵO a%LJKs!ֳsgxX5/@ @ p#!fDRqjU %DA;kn4w>dڸ54w*ǥ@{ n.$x^VI!hGF1|]*[)P9 ^6<#"BSj]RoS _j\RY}{/=+pDVf?Ţ)fC*?(!.~T҄1Hء.B2VNO}V@@*Ρ˨9n| Sɍ@gF.bq^_~?)(;awQcCs 6fA݊~H.׀@?r-3B `%؄Yv sQDۘc(P ?pYtPcf;F"h#Ѱix󁤶A@А?Y^nwO;i&*@ShV0\zx9?-4> 7qi܇[?^Vr‡  dDEyp;Ԁ'jAu/O\?;_yꙟS$ Aㅟ6KR8U5E\69 B|wq HO)TM;‡.6pǍ6cبMy : "n N"zN;>̙)ƞ5Lw[Pr-3E;:6(R֮Qv*,I\0πt $306$Q+B8-ڗΝ4lӻ_3 OНDw  09a4RTeG !/ǽG10 !hԹZI=l:`9{,ު?2h=1E>mq6b]qcdޤM!%{в+i~d43;)7O Aځtޥ{@؁M>ZCvElބ@HաdDΞCy?3rHPl!5PW)` MZ͙èU^p@pf !B?)7 K$4dA\ %<0SfSC[Sn/GEG$q%Fm8ۣVyB4gsKy0mQ!!CP+>;fD~i־yFt$ =;RR'؉:V{u|| @)g~9dp8p?X^{f$L#Fw ?B*wF}n6IkIst~6>1,z2Zgpo SNqgVS10k`&E/}(SX@m}fLE?FRh_]X 9?7w1B 7Pڹ{ZuT/%cvn P*5Ql dTV%a"^Oȯn-Z~c C[%l,\ˠG(HZ@"u \ $lϭB:̰cֳ ]Qi}~绤U4Eކ^3YCOA> 躼NM.^|wj~v?&uw[ԩUYd\Aӟ??4^~.0J4(a8a0GPn/@o 4!lWzLh>7Ƀ53?"OBe#hm|ٝ?#rm`5&D2i&K`DS=;frp츪]K}G+ 'S=IAmsG`B MJX0!UK \ y ͞Ok65=hwCWPn)ͦLם~J똜*&Pvw2bhT 2zb i\xhT {7B@ P  j]܎)$tg@ĵdDfִ4*Ux;~'<2yV;9fQG2?^q}?{9LNw ם4+ԾrPڣ"MT Omh!)?988ZRsz8{fk.^xigf.M4a4K+ib0np^o]v}z*B`LbtW.hW1'Ghz2ǭ9Sgc@pfL,4J{p )@]"cӻlHLyӮp&F,5FbYx4BϧX0n͈ҖN$)@?E2HWRFЖ?GM{ fB<w<6f'$BH6l1Bahे>z=9@072 C6.³9@<}c+^GjߑaǬbi~HY֤O?;wDؿp`2]cZ>Ҳ/!;s8|hkkxk 7P7 10 YspRaH+o"]ޢٓ[q+q4-ʙ7Ph~q pmG{2b`iT.xq9l[@|,.暩І 'tD" D^\a)ԏ2!yv{{ͷEةx2: (,^A.v=JcF@T+>"CڣZQnT3˯~@Q+rc?&# 1)+D$O; V{Lڬ3 A579ng#кf&1ldA ܡCn >Ei>J{Au0od\.Suh(+f 5od VzKv?tW0Fxk|'d'ݏ h!: ܿ9t4B05̅>P3[@ 9x*z.Ν/1j/I܈wN]Ź] S40BzɹҩIh}~4Fkcʜ/&#HZD&Z(Ֆ p1s㨍gtK￴5оUS. mտ{)ZasVn7HGv|k,:ew-%ĞX|9w Mqx*e|fW[kn_k[~w_ZМ}ylGT60bRRiw4QDu ?d=IH*F}l!AJDCTͪPkZLDі6t$O @AO 8SO3M R (n'Ai!D6Bade)(w}pgw}^ < ZN Tm2t8\g}e9ڴv)?ǝ4^cT- 窡geDaHueW'HM*J5Ơeɉc?0i, hG)r8BKvkEe7}Zڛi?QsXd, p}z= G,49YUkP~*~؍[Ә#Hړ!u96kLtο ~-?Pv+k$[h"b؜24]'h#QLb$!xC1m۷9?YtkwjzBqvm4olWq.{ ?_xh> $ڻ?9m>T+1!/2d! C1Hs 낌i}sZN GwJAk~?uGj;)ĹT} @U?j·7ۅ#b 4Ky @5G3}Dq2rlq1]=sslg#GY8BO֦` Qg  >ˏok%= ;Ш- /;&t $~ػk ~Z,i#0Y:h4B?Bs==@r!=\QC''¾J7 /_Bo ;!.O!#ױcUq݄@Ιo!c>6;BU^5Ji2+z>/ r֊? `7q\ @ g8 @ PIǡuZp܁ H&*0i(cy\<"A,h/YM[߽ n)-m3Mmh_-ӣR!M "~R 'ŵ j@mts^h$h6)Q4 h8J=s1vv7Ji\pJ٪ᩧr2~o( eyڣѨbPE }hYOylp3Z3<+j+aVѹ.T:5 ,s'=">ݗ /oUvס(K(]} *Wנ,^*6S8+Y)TB|-ŭZk}]AB8,@%Ы]h}xKj*/A@#k0Nh-uLLP#?XF!Qƴͺ f4%7PEbَ;żr)p T^9!Llt2nZ0O4PU_j@@Na> !@B9p)W!Ѽ-wgV rs<u;߂q7<0!:Hȵ+sh 8%VYv#>H ѫ3 lG|1[_^r5]~"Wj7kn.F*f- /WtsҬc;AzscgQoּ_G9?Y|, wsq=LZ[Q*ur!EbLiQ\P*B<*U!# <%Lj;E[y-a$~jj58}6F? Ϙ~2nM  F 7v';̹Qig'`'|FM} XBrR;|rh$nѣimZ ~ g :nxY@_?r4'xh~BPqnj_C%55qVGFBP)),/Dt^JbU)qmi u;k,- }Ž#,SxsX+dFo.%gͰ+ ?@29掇!DmKs~݋FY~e.fllɳFs .xq =\HG­0i=V94=m:5㶘}@ڴRIMO-vkS0mҝcZ_kGha|A+υ<4O?< >sQ ݏ!O:Jf<xd9F4R$DXi&,9>|ecS$y  N*T} @A@5^V74w:Mq ZO $?TVo"Q U)χricg`701GNnHu 8~5x=WEE6KCmlΟɟm w ?%uCN a]}?>%qD VuV8<@ `ޕW]\s$tH8퓒"GǬ_w(2:4_r#V* A Զ\iIt D\JyPVp v9Z*| Sj|LjGm  DžZkY!!A| ^gAJPtBf,_iOKiJÕA^jӝhۚCNhypd_< ayGQ ݇PqY4Ou1J%8FԊZQ.F`b^@Q^.ֳ!F W,>vM7<ˋݍ#GEQ~WUP ӟ"燣 ˧_`ʖbGw+ܾr䇑Xup?|6sC-3f{U.v:kحAkx2<+дvvS% p&ƧZ$zr}&Ġ3+ Y;P (SP7!#vr&6I(2G{gIL4 pviCJv/(3~VQi BC܈>샐wuLIbC43Dm#GFJ(Qh]蒳G@!Jڕ D"6 HB# ttiT?F?. ƹHDco(ozFE$|%K;t\vǎ 7?"46{[x'Y1Z[깷VUQko",M ғk~AF-EW|>!s1}iC~e3cFmS7=~/"17ƾ#m$RD[p @~J @g:!vF)hyvtVr(J}#|#쐛 9 :ڈ@{6/SvBNc 7P'뮠ZHp2.M4jU&&ƛtqR]8thv*5l?{!\6Mr~|깷1XB# hѦ !@ql7P©o ,<@ YNaw͝Lڸ G'!IP6y?TM#FJG))KƔwק!@"arqъI"~Wk!0eH@D P Bj8 9hqBv!"P\hw[^> Pʠ1eK+ / K6ΏОY.Bʿq4?X -ARœbax$0Sn (O4 G.IiC ] 왭Fd[q!%|;8!g5DK ЙTS7i(-Uѽ|l1h]<lk3gv\*m @T*& iy GEq~}`H΄+uKcR购r*IL".$XY:~hO(8r_S3-sL~qq E~ϥ@ 5(A}НPt*Ѧօ# fV7Fd!ESPQk%UU92.ͤ*ij9\yH*VC**9d:G`x*q'X)v%|@ 8@ 1& 14&ZeD4t iɥnW}.݇ʶIˤxI::nI{iu5}mk;+ՠç[ {j/-PFP |AP ( 7.l2SM@>],ѹ^sFn^\Zva QtJ"AIr:3_#ȬVR?> $:2x3|@p_,c#j$YD설R4_rBQhcmp~Ka/\'X&h= K&;@*V Ͷg6Av\mq8φE/I֖rmR*CTPqz{ӟۏ nw:pHF<ĕ o{ gv1~OweO-FِML_nO?<Sr9L(,5Ϧl俖@U㪹N 8Ԍ-mKh^Ӱ)?]qA]mVq>MǸ~(lRz _Hޕ+ ؖ @)I1;f~i=SeH(S|ʕ\Ԛ#rO2t)ַT9u<2Ro~F 4赊rx:'79NslixR ;_  @`p<\t𔁣u,.){ֶ(RM-}㷜/V鞢vO0x~ wl*ۤyq+Gx)'J~\J&xiyZjlJJ is:> X 8y;r{ќA4 $f0C@D*௱Ics /ͣ24w,\LYc/.ٺG*dXF{1צ}\ڍ=kVϞ@ E\FQ'm K>*h]?U)'/='4\(mHZʯmrN+lid r$)vBk:]/oS () D9F :HV15r!w- Hp~o*f{]\`a@f>yd=h&Qlqtrs\]κp5zvI}=*W5gY9݅݁8/zz*XY^D8qz-_v+N,O/= qWk,vM?Z`L֜8KHػN  Nۂi}tBP̮=\DrCپl5>rAeK-e &^{s` QÝ=P[2* [/ݏul) ߳u~@\BܚEҞLڷARU8#qʬg_bKğc0r64غŴ.9e6!l5pMdf-'/fd* ј9m,0 *ev,8u0l,BI!an 3ִzMB㟉CQo4*}"z\3&lo0%?ˤB>\-@* ,@|4ͥ>3F VR)nNhS!G?5 =*T]S@/}rx~3L̝0ODy?QtppB (!# c+qh6$Wvۢkg{ Wnh4KChma,(2@γ=H GVҤd!`8p0@ũVҫ] ? 4fo~?g޾vڸWzxG'Tg[|unrwWS\;1L;'GQ&%ZFN`~ 2pFN+/ ~-寲2p\tm&p8hH!v+c眛 ==@0(99\qUࡁ\XD 3;{ 71ǯ>ˎVU)AOZYr%&Qa;^;$E`;n|ݿ4p8`/~ZgϯaN3.\9{ŽGww!^|*91V:C{ {ўoi6lN/`M A K}ɝBK( Ƅ ?rl6|zyyYǮ`Le,ۃۂhs.Ҙc;sn 5УYj~Dv5fT^(49??0E:ʐFA }{=t n0Ҹιyٽ܋C&<V| )\(WqpNStĠóϿ{W8t(jZ1֗2 {+a)@l2K"UܛT2Ӝo\FXĩH'YXpWnwFr!}9_ v^Р}#uCb% Dy!.ǘ?48I6?B @߲A$}'=f{{z̎[7{knopIDy?!(YR$@QEyػ[w}3e؊Ψx=3{xUYYYY^׷?wqKR!=+E,'qT,h &OH1B ڔZgLj'{ [I_#RpQM$6f (xVA@A_o9y{x|8|:(:a t -l㹻{3dliExgC>CB"F2e<*Č||P5"Pߟ} |:(uPY_Gr0h\;4q=h[{גcvbɪh5t`IC5:k}B9"PCU)3Dp1\Y꓇Be/r͔A?#3,/A^uʾٌ+D8xy}a+o`|m W6X[qxO{;A)b}T8$=A4eeGhI ;Yڃ;]bmuϺ\H[w]. &a,=.pl67W+vx7"CLNM8cd?g["qә9zJD&}PwWZ)8*@q*(Q"E@+HtR =6gր|*(Q"la?a%|3r=3/! HyCD;?4px<|_Gx>یv\5X7m !m̏I!vfފ?ME;i2]c349F {T?u^UVN.?V&a6SɁxϞ 9ׯsR"MS``>&a2?]py^σxn7޽C'gH&+ٳ=G '1Hx$jx<`Y/r}y~+oAU@+`t T5{l=v別r9y?>9+JP#@Ѐ~CIiQSY1 k}Æq:ҡ|m[,nhNˡ-fƍj*efin+".?`_׿Ot!G{7#R/PH Q J~slm:0eA4dB u5%hpޝ =oe Rf+ x<>x>}PBEJ8KT+n0g"*#"Zͅa恺 f(E5988F}1[͛nH8UFA|TzAIyy+{Ma?EÜ߽^lcr g-n,IA)zMJG`Ȯ8nga?KgWorfNN+ϑE4}Lg UJd7(S~`wD@GSs׺Sg C,g~oh7M RbY+4zrF {l644Vs]|s󇹎Hq]?nGvFH"׭gn=م'1,[Ϊ[je" py@*mbWA)>)D kW ^U$LD ]m_1DрkńӨ T]Ж@ n@ @5x>]PAT6OSj pg9"k4" Am7'yA-BoT`>4bPor\ThȔ#-UנnҊb-6IqL'2`ח//ާpx+,/ 2 2t8o{=6{=ِ=s{|rzv;,j;?C3NZ/޽F{8RW6.S XG[ t*h0.l7+lǝQ@ P..0 A]pc+1)yzx<ևxwLM7(nji>u]qU/^SjA"M.wnAn'cr!p7[ۛV%_`Ieޮ?<;/_,E #2Mt#/)|%Aa ";ʘi e 5. An"J* s0pcoOk APQ?Pkvz<<s0 8JM[>l@rB9}QM»9m>*ZʌTJ̪T]r[\=v0VHcK"S%fUAh: {hv??<0.uwLESSN߹VӜa3I!tI-,j/޽ȃ;?ͅ;gpljsش6ub^n@qYY;HQ te-{(C%#BaC5d]ܼiN,^4u*uAf^(UgktQ nn|EL&GcFfNr ,gɊ?kZ3;1?|#/޷ydP{+1(/>KRP!(Mt ²At"BI'5"[T`L-asZ1P7%z<gJ(x<!:fA)WDhɡFh;\a)=CL#-}aV T}prXHLC8UoV8 BP#^V;2/{TFX R*v.8J3>KDԱKn-Q,r*aJy^w=:DmM߻R|nWwoO޿;rH89}ulљ HZ|yIJpr,KåǍR'ع8N 6a7@p4cŎKI ndh}F;B#iFu|Do'1'AB} "3 0$>b`bZ D") aaZֈ ̼u0m\n8B`8/aHDZA@* @ 4P{@lQ>Rz<wy<"@A >|4ZaKȊ=)ZZWڸn.t\mpTf1" R7{1s*[w;PGrC=A%_=j @B#+1{$IhZt=|mX] A+2gE3"sB~Ϋr}eAtFG ;n?ѮI>AVV@Dv@^+fKDf86EԺspu7JS8JuwWcrE<^rsR5RQuu(?d`Ǯ˨>}|Ja`慂2+?ڻl>gNҚ:g] Y|6tGtΧ PA aA A2Q]>F(K{ rXj2.L{3Pĺ,9rD;8D@Msxx<{Jvߢ NNQMhNzUWuCAalcSaÝn6:+p5<˃qk. 'eDX8 d.,,6yX;$8NhZt Iq4L;v'񾽻RzK|x0 Jl<շ"X׏V0how)<< }ݮ|+WR~=ytUK0 rQi\ks48I"Ԩ|q9H%63cUgF ;Ԏ!Ϫx>=Lpp- ._xfjw#c |֑bHQ B}D D&Z袜FiSJB(KeR"11*0TE|:r(E4-׏m5k]۷h~4m}"L ӟi)DA]~APQݚ̡=x>(参p2 rR9CCSdNtG4+4" Aؖ$}hmҰFSS%7"N )efs2&GY'Z-NlvMӡJHZmEĴn'{5Ѣ4+xQaqi?5a7 ۥ.2\y"]m~D3ov&0S-Ƣs>2ƄX1YI/=qT[zahD|:z]dT8ڱ鲍g0u5"R?]Ig{(f8a7|_ƣe'lfpYWCWWl'5*]@ݱn)ۭ nKGP+8?Q:/{ P_gM~Ѣ|,o) V ]NýI01y;T>M(dJ%/p!DQ["Bn}-{bxUlXS luYM]l Pnt3,ՔjQ'PoC89p8v{Ymlg9xh7nW8?e ( {Н #""`EFɰ]`B 3 צTq[/6 wKLYWWEؿM(qap?44#"Po/0_"9{՟s;x<"PG}(g05DCbm_yxsxJ#$۔nh&79)R7Mz\'q* ?}}(W2˭ Dp$^PkL>2i'!ְܶkLG$v);R1ā}{QPZ oxp0RǁgKsL]fH "&"thWr +긒 u&@s8}i~֍pqG,51 kaWejk? um @;g\h)Cnwt;t;mZI8iF* >5˞1V| cE.4y"/(]h]G{g > ~,fy"7bGG@kE#NlǎqiM̐L @%V?A5`lTB3iDv>?o6 ΠZVham uyvm [qfq(ӸC'v ;@pX{so @4 N(94 @dE65Ӛ> -9&[E)KYuMDhB0n7ҳ6e@I>[*a(Ѧ*?:#j Ԩ+"f?rgxx<-v!#ۈ)q8*AM E\?Ըbp$;& Qjl c}4\"S/"G,fO2ժ>mr|7r Zt:]:6nDŽkP˄$| {tb0?=6n<'g!E |~ r3q0-h%n@3Ep5rSC L;QaȆ4qB6p{)eΫdwx!ܯ fg1Ov=of/"=*fkF iTuTb*Nʊ]BscֹG;/0澽)a]2) W$ۼ`ş!/vXO3O_ʹ7F:Dm0~1Yo{J@b.KktDן ꌋHP\230 d;UELјqmkRvdu⿛u@d㴻m: C[x<3xXܞdxR~q? >@:G |f;F(2j[n+6d6ܛqEHr^6{6v d%@9&Tχ#4RA*B6p_BQlx</y<Ij (v.\R+ ȋu>)m77,7wU?` ^o#m'YVq__8yAe8="͏#L> ugb+1KNvJPGPhO?s7s'!hf9EFk*(jCwq~ɋ\Dn|H.賭JUA n6()!14~  "*  Xx<^x<g 80 bӮ;ihN@ۭ2LZ :f ,j̶365m}#_lC e>${|@)k闹JWG8tg@?}|9*K_% b Ei!*8yp`.nN8@.H3M7#YPҐzInٷf&+ W@罝Q*Mf&T'2SN=jA0U惴e-{`x80p|3"a#+')$T u.t!##UEmiRf&pa4 xu?GU`4D7?=jpO2VAaA((1mD4M|+|pp [kTd 3_ 57]f?]߆le@-`f9F҃ [@fx>̣}@m7 h -fš:UyεѸ#H hEu؁YNؑN 1d9~^e*z_'#uXL=.y=sM 8VůppoaZ9w2XID8V#B;Ez]Hsd6ty) HbNޥqܰo:[+|pvH8>ԝ=+Odlݧ`:RrET%$^rTBk.@Z#EޥgO /:h3FzFo^v]Wt[56vw t8IhFm\̊qVx> uӸ6Ku0 YAai)#s!~tn! ڈ(kG܊6H]`]*RVRYW9{@`q.q()G?}b3U 0 .mk{x=<dzM#])©{nA & R7#0 gO ]T+ڃTݟZuf|AC Fv!1AN3Uj xןsO @菂xn] ǁqjlPY608R9 t6]FC?ԇA 0ULE&* > P\?bbW'2A&;ֺ.Z_[}N@<ȏ=@Z /~\ee sh WT"KsNLcs3-E$Qh]g8aPhM Sb wy/d?z 'P n0Xyjgy~PyF{ {1.@Nqs'LdM߄7N7$kj>Lɲ5B:yj7%o!vHp^ȡ`]&mdE?@Ofb~ 63Ё._ qW/ ' _kst_Ν Q4PaX]39biB)( t9?YNEN"*Hs)ey7#a'<:KG (u6ø=N4aPii܃_%jcpYhAT=/Qk _l86j؛K2J Wit @sQM2Onۖ GGm8{m?CO0x>?Я&:ݸrC mК:BŽذngl"C)y~( e;Df:lb`0sQ庥0s֊R*lA"0y8Bj;wn]-5?Rv{<ϽEx0!Jb96nqrܜ I2K5nsi(eLPeNuz8ml։-HdAa? `v4 ϏSo!l}e)+#86I~'bl~L33rz!ZRQ,GFX -(D$ItڈnMLj$nPAЊ#4߱#(_yH>GP(D+nP5CSR{7˼>SG)Fku+ԑmpǰ<3=amг DҖ5,(?PiRtců%*Qɽ@8ҽP+¨@!qkSL= @A׳ç<cu5Ь/0χ^x<3JA2[ m^eټ<5ͧ5ARm `#ذ APf*SHb*ST=ei>eCyP)pJ_ޑUFE [ 6kd 87 {~OkPEx vVp@Lq(19e"tՅ|%#"9ݺ&QnErρ='9{A˗>Ϗ|:~m s=,?f>?ɇ. = NP*$w֧Ȅ?r!yr"fZh̲$'кJ'r Y1JP!*OadarN~ RJ!K`k l:zN* ׁ*+x</y<Pjsi**T@dE(M3Ln~hlgyC.KSvmYM/baqLF00W} <~z/  z1/t.C^|*]7Ba'Lǿ/su`nw^?wnCs\J We0ddYAh4qJag;W2^y#jt7 HֻaҍCĝ &"a`o=Ŋ@c{:+l^;gܑ@7ƃTz43UM n`p3<@`J=SxR+X)UֽfEKA1UMO>>A֑X\ad<Y4 X&?6Ezu%sIJ1;WNηs\1Dy"VEYbP6ԛ|:XqĘvO(2( P:v6xlC/N:q?1*W&I_ZTyn돂x>)b6:KCIk S٧OrY$cZ Ao,1 yV />4; R#2vm`TH%+R~D>m$ӧv n,iZkO;Qoc1m ;? 81S ;FD\_xp7޼Dڬ ҂ T֚2_P1rpf', b=L26D NН(no'7|wtlr! 8T%P;zAYLE'8 Rl[VSFZ! 'WO5akD11ǽ3qgt~6|_CxzЏ&";p6QwReH#pudE# UJ3A"P_}@ ZGE- L̶P%>J}H}$pޖyXg[(<3q 8j:e`8졵`mj6+4=\7ܛi#~ծ\$>,Թ&jtYp"A8R1i<cj]2ލڻYL"RN+i0Vy\=;;6ŏ|gXZr..\ే績>4drįSj([ / oD7L!9אF$-f& `D6]mf :Ez<qs!)=!Dd#]=4Xz 7T\ԙ!l"H&q{'ƋHn5utE)L;ql E164"Ng H5ΐ|)JgzQGɌ -o8BaKfɮS˯ooS oz޿x<^x<,J8G?aᆃ!J5E{svEqFte'wx>;Une={T)N3wY L ^Vo~yMlr|0j1~8ʙjs$s`;/w1zmCMShͩc3c݈#vQ^^-3Ν]f׮v)zIE>N)g^br;w ~>v6sv0y~M.; 6プN_xCRҮAMm0y}8X "2Z@lq6tE;}JCknwKaވ}'f9p^× \ KxΎc Wsz^eU K.Epi$GvcJ?Sr|Cڝ1DS괄#'n _|"+B갹E3n,˜@3~uNfDY|tf]^Xr-B)~7~3ghFE=$Qk3ls>ݠheﯝ`nm PiEa;Tg5$m /E)h1bh޶UvEWq'["1Tw&Ѐf;\6x~H@ q^1Nϖ䢂-XYg>qB3>m06q(ZV (@PdH"Dp Jfl 9 { ty\DgljBe?A` &'GD흼 xH@i0ј*527$2`8\Ckq7}ƊCۡ G,"ݐ-&Q>\ 4PPj-— vQM`b5 7fx@AOy<' c[!xuׅNS+Ő$V ,'3<+=dYy5rnAVb{4`2Yv {}0 h1jT29bj2Zʾ=c;ʜ@g/Wٻ0faONs˸zME=ǥXUK{/>cv n4;VbX+12,_\cJ' &ܖk1g E2/()n7dz%= ,8kpɋtpLm\ sDu T-1UkT UWP` eڸ~GAA),ׁx<K(x<^d} y HMwM^2Ɔr%)m#:cW]ћrL SnSXΞ>,pF#߂r3SSOH1 ֆQdyNYĺFP /|IuH~VYrp8_], S;X7& CCab,&Nb6{E IL5mB9|`W޺1 ^}:_x`gRi;7n^wC jJ~#z#Rcחj>ѺЌqI.M>:Uzu8X *M@s-- dZ~F&qEj:E˶6t݃=&XB$NZT)9Z+Y1<[D o. P|BH\,-Ks[u$Ln#LA:xn)*K[ lRl)yH1`7H)(ߪxx<8 P`iKnwTCHU4w4%owFkw%1SM]#D(@΢縜HpVfsmcuhhGA+Aō~? M$spQɕ9ndiN+I~~f'LLef ImT&[ SA j3Lu{'peSMnG N?[ Fnlwa Y{OX:OSIvv7-u Rw,B3:kШ\x>: ZE\˾WzaM5 @: (D?݀rlw;6mwWy<$&;1?w(5>Djyme6'7 .k/8ne7fBmA" 8k ɂlɳw@o X7x<Qx<ϽNc(5koR,yҶu)4b:49"ʩ]6nX-I<هXkɊ\ .+YZ'וO|XݡՊiq kW7ҴtQ9iC'&B5RzUzÐ$i>:T>;er):c(n')B$$ILoPiǴ1~^NwcV8?5?=K6y_Nrmy?|{$Ϗ%]_bw6[8yjhRL'KH])qN<TC5IOG(6Aav~]@qD]'P*a["%+x<??%91Lov,"ym^MHC2M;)a5'nU9 aYLɫ:hτtS)vʍ uy%~ϟ xn)::j^WiuB*~v 7 {vGj5ל#/ji LZ!f \-QbCj'쓚UN-|nji;2V+VQ:KVQ TikBq82?x?f~:e|x =4G+ KPQ2M52'PY.LNP(fvf?gjEodoK+43ߠ=g{ Vs%|=S6a\8fi*NkP6hXa,dzxEb+4L^,rs5kDe¡6=&[w|ey<ϭ@8߹? v_?[@Dpu3ݸv|QH͵ Ɣ w:&A֡V]hS'F)L.JdA6$o!@ so˿⏂x>k1P N[70M08[ ZG<n\،EޑtE&=>,^MH<3-ݸIcvt.NvE+VEͼszК,?9&mh-_x Q] ˆGK'#VDB vDNv+bbWEӻ:t!c o]2O[\MpmLOYY'lm%)AKJ":#֨i3R!sضFq^0˴ô9Xp$h/moߩ?= @-(u qQn[p#Q h- oޫJ%z#?p1$VpC}Zd/`wqHZ oΌ/;r"Nnw̧M;iTaׯov\eyv9y,7N1M" "8"+Ǧi'e hbZa*Khl,a0(Kё[u=lsS.&4e5Q nZysEQe8~exgs|eqJJK븗 LNACaq,ېܮ{GGW^.\Z >Xqh]^!3DJ,]~B)PtODQ0SA(UB@ DT@c BMhC"N>@#sUuT" TQ JO}>Kf$x?͍ʫY{<x<f8t'Fk8E EJ7wT:N`kFrwDP=pd1ӁzvGUS};Gcǣp73~GTmݥin}Ut._./z*8)A)Ov7jBtQ7iYlQHP^9MnXjKGP & GZ־tq}|s3 wXڜ&(",hu ðoc("4= vX)r@t+}K`@ RZJ y q Wi=3Q[+)j1=ϧ_N!jucH"Rٝӌ{'CXܢ;h x@Nل/"o/S /y5FAl5sG(źk TUر"$ƶy̴mʹ2Ӳ5 `SbC @m# ŧQ$"ĉ~xӟso+^x<(E=J՛er@`z=d0X%ϴ]n&tYиB\E}8%A@^l-uV)*Rqd<ڢoM?e1 6,^PjtLXNG:j%BQڵ^ꕍ_} w~D?/E02N"0,U6y]e0 K7_cwg8r顕E-E 䪐<F,@!Z\F_LfU&%ك[*U{ ?M2 V;w0R9e|rm;|,n~x!Os 5nSO9úg-,ei4^DNL`0uV1NYG`=XQ+yGeRwL5='~PN, x<^x<d8]/[珝lǎ"ߠ@4c#rc, )S=%i%ʉ`(!]6~qQ ׵/ߝOB %X9 |=iwHZm:V)uZI.x ×w$w bqS:C9a C}d ;F>{w((MHRWh1Ԅ|3ٰ<6^@ffCv"[|W*#P Kg +I=x. !˺#i*x<~?Ue;@q"$ ou  - 2n4.S'6e%H dG pCQuy ڌC5]Kx</y<s/0p Z/+Yj(n]$yBos}@Sum:VYi~5`v(y'-2;gdv7v' he5BF?/ϴ≉Nti9:ΠV¯/v[Rr c:mE$DdmrnܼɁ>I$mDҗ* b뽓WߐvHt\ZsXe-ZO'ǂ8BRo?1)@D}J5?bӬ#pv!fI8i0~T%એx<,!'^̜C?twri~߿Yj;wr'%j_mzU5 H^8N# A F2H< q0Ό{=ӋZVZK%J%T9Pu۔HJ(:K-֭_=ss.!3yJȁHPq}5R ;| 9!$(·C>DR|VeKWҧ-h_\^g1Xd1I"i]_H RMJ"pcT3s!n|~W+B߮ =N^ɩ\|KZz:t7Ueee'f;D$ -aӿAT^ޓK >M[\HX+_2']N@yWG(ohELhQo4xcQXH_x@ : "PV".E @_l5 1Ƙ;Q! JA U; aՕ%|@#2|aWB { @dق,jRyIŗًZqDs!JqR3#7hh5T5jfYVmXz__A,{ohp(= $;^x5a1 C~8'ez[@m\>T\PuD\K?GB"*1N‡:>Xr98㫒Tq+s@*ޯ,5Ƙ cFQ=2)8zÙ,_dmy<;`~ Zd |߀J69-e@yєve0+wfFF>^IyR7pO'?n杽|q>N^خ46Mߩs~ֲշ\XU"D7GrE–o!tx~=:*oTGK.@(h"o%ӀŸ?Rž0ھԶQ8ٹ=8636c'TtA^o]d>4ǹ]5]DhW?.(#vu\D|+B498/*s >Tu p$8y/;@A!G0 `7oPN57[X3o 8˿1! o!RGU^H ;e\ Z) (9yL _#c@Ǣ f~W%WN>Pşwǁd y ?Dڳc^0c詁GdR'z|%u+A40 [N$¹@a5̺EF ! ɧ9~Ӥ(3Ѷf+hG>YqYT/JĦ=6k6={r]Θg/P]+6kghU$%aY-%30lγ+(ʭ)X8<)!Dk1$6(MTuK5{7ƘCt J0!?pMzb;-@8Թ )\p -e 6p>0EPr>AUQACxdp@+VUHcn$}dA5јVŶ+[D]ZVbYM[N~ɗ&#O$ wnZK_vPƙ:󃶳W]:NU J.v.UO#ffJgтgP4no? "#>|JaP9ȶLj6n< og/sׅ@;@?^ hxx HC]o-<Wꂷ .yP". ?$oFծoKN@/3ɀ ,4ƘZ1SbH|PR`?f-Ns:yVP2'^*uJXm0i-\ʫltibiF?sܶ_};\T+8TӤe G|40Av6?ujvQWXv6!I摣 [|eYJA/VŗQa0nL?N}M*> BA۷: W&ndMZF3JRc8'2fg¼4V$AHɢ\ =HXSU^M$$e$8($Z݇ogKATVmo'J/R|OU- DϗT]4Z# Y/%{7Ƙ/Km 1HhP~xh98X<\*A樆m *! rE{6|Ey(U@}~H *𪈼 d|Nd*ȷ@w  ?.ٳcch r ۽ !07*IyY[l}|C^e?>pN T+pt]ˆ*ѵw[>Xhwfɏ?l~|r]],Wq@junǧ?@mm"J*tWi/^eʙ[;GRwbWk{%QN)Y++œ"^r(z}8u>,?C ꟠͛\3@eQmR4k's$ 2|<2E\g1Xd1|MGZɠ!J'.2>)%3E( )Σ0`I8H"_c[<,L ?fٵ+nue(DDmA* Oyfr?n C[HnX?1 JD/i'"`['qו$V t})t&EUϟҠ& $%BJ<Ϧ)[w$f 4 \r{6Ƙ/?T5 ~(CցE t{r|У8.ߪM*z|juUҿP,3Bۨ~N ,G#@T`j2cR@c1;ٍ0E3%6r'uX1?U?~:?W*?KӧkOS텵¹>rnwI*յK˫s۴xW3==nIđ'ewRQW~vϕuIqi֛@(o3RFAgHb (p~ys4vӲP ?9S1U %O%UiDLZo l)e}9`^1+@xW/Gb`poMP-׽^$QBU?Uwãm(>uVq$z"PtNA9_M7)k^c1ca "@/A7X' p<ףn:9 HqA)m0hn8gA6_጗p:%?`t{Qbєyh*bNw宕&Ǯ;yLvnR'rqnU$l w?ӻ3k O<spVaz.*>v3ű7h*.VbKb?h!s@,.}=ٜHdl?Z+[dz9ǖsJ ?Gm|04h>QXVIޜZŜ<1Ÿp8,˱1|C_pxǰj|@h> [Nq.(W֋ˠg~\tG7)\Aҫߖ֬sy Z6zmڳc|^0c~;-Ѓ xfap^q-L+:QښKQ|EjZE6p@8?>E3Ev~kIwhg0ĵZDOUΞZ@fJo|ۈ-U8wYIljSҊb;Fٕw~3\"YDywX__9][@U}3z:Q4ţ}o~(.k.ꌈWՏUn|' `¹1pʞEcECf 'S(hoJ3::YT$U|+9B" lOoXVMT ZEyh*rٵՑ$޴͖_c_V{mk36'riN)'N9וz]xp7ٶIYkY>s+TAJZ Pt\{.BD r@4cU 8sAM AjTO뗙ܒ}(IZ!n)tdk1Ƈ@}sxC&eNh:(u. }s  ET;ndF5-EdXv6O.?c1_{cu"]LD!!nwY]gm%CU !)ZnU> žrF^k8NjJZatcNFiֺj2.zɫWf$ZF]kwOg_}kn J,-XQ885tN&2̐H;Ov竃Cz*OO:?g PoELhm$@ pXYc1%!P1g$O@|p,65Z EOo.Ƿ9γL [WC@( ( }|%]_6a#㣈D&.pUc17>| DArHwxXnUUWSͫTS^UTn\hۊU6H[]|r"($p 6N ,ٳcl1cn1B$)#p?{βEVsk}`;T 4?ɯniH5B5ki-VT>;w"ھ1,kk+6௿'f=([nn.W+^o%>*ʢB ChE.gG^2 c9 [A;J~ހU6m%SCEA.'g/cnF@m5b!ZC]{wM#@D9ր'n3Ƙcl!G(s2/qXo CT C*`ݵ MVfkfEقnI"JQGHr_pnFGGnrEAh*Q{+()= ,0FV|=5AH +T˪X& u =V&v%Ԇ<7\pK1Ƙo{!BhdDJ >/ i$p{ vc@c|n+l" o3ӑ!~۵9VWs >A9aT(X{n|_H##:hIޔJYIҤ%iJ%Ml|6H("43p$I 7'.pB (S*/Wv&#d>rG0 ]7h9~[eT+#vjݪnC[ri1fOx8}bԍ @a]T@G6R<<$H )Hoh1|c1Ū)P CYAe?P Kt; ۃKY]R3\rӾ2;w7MFFFjRKBe* jJReK &{wSW>͟?STI&;Yg0(Ǣ3{eMpbZ繩DRyaZ2 (<$CW Op=pGc1~"Hp#m-8V | N]ǿ Dxߝ^]h1*1Ƙk8D 9A^N˕vX_MD@f ~ev$jZA4MW jk GZᑇ&er'LmDQPo4t:UuGOG*?M "RPQr,a<ԀYqj(Zyt|rit jt`챱Y@-n~"Hzn=6#+᳀~fcß}Eܘ_RD01-b[|) [*' sNsd1&$S2cn#AفH@@l֏ B%IPT(PO3݅[j5Ңhvչx<4-*tdOc7^Si4tͪj,--h49{ 7#MSzsɜ❷/\  ( C1)@z|0\sQWGW:RaXo괿No[>ܶbvRk\!B 9m/c6L۽x~ЈPXXv@?Ng2h$AjCC [ ~1X 8c1nh a !@ =RvY޹6ˋf+>)"20*ʶgׯmO5덨VI9/t4M(MSylflݶ$V{A~*J/9s47c?pz/篴) G?FQP*?M%OeXRBc:ʫמ{anK|8Ҫ9*CҊ`Fk[jg'1Ƙ ?U3@pk@N<< QUn` o^ZpXcUc1w&`BI $ ~}2Kt_.?mu*U WmuGґVKI=^Nt٬k-S "^ʕAP5B9q#9p7YY=+?amcq+U򤬲[EKXw寷=Kh_5yYV97[~V:D_t5uU%3Z['C$[cbj dP\j/ucn2"yhrc^QnAw+6`hYk$Li_R>+Pף'A>c,2cn@56p߅J". +&N{kkvQ !UxARs[Эzdբh^1 㘧. <9w +++<'3K\:0;wSfu-+:2fqC cU&*lRDN9}_v2' ]I~65zE`)ϯ?#9-NgvqS*lg$N >hq.c@s+ w 0pf°(\\2B2 lJoV4FV8\DJZ]Xg1Ƙ @c1$ x@SZT?ӌ9:#ʫ ):mpga|p^OZ##uF:kA?;v kkk={ʟjsg)EQ}S[977UN}/O4֎"jՄ,Z5Ӎ(FG<|oJjee_J/ Aq#γ_{@Tyź,+ C(8Ov5۔vѾ THozonf Jބ*X_2,+ jh&>F %Qo-8 qسcUc1_=#F a9_{'[]d2Y XnG / rouڳhRZ=IPU._ȉt0:: ~&&&YͲO_zv薎DW֣R(gl(YE4"cKhe7Qz9>YDb>goLH#]w7ۿ=Goh7ʃ-ɇHW7pʕ`fm]er8Q v:6.71Ƙ ?/ܘnq`oqQ|Q} QT-Am()Rz2`U rE3( @ %1G U|(%HT>QVJ# -K8أcAc1؏PA TAPT7:k@(B@r Z} OdKH&j5p ^OM%<oOWXG9 >ڣc@c)! i044ȟX]VXY솳0dB~A$ڏlcjZKZ38עi޼|L}sl* G|Wt+kG]pfJ*(ii Zh+s ZvVcd|M4v{3gt{ ?xm92+8 ( j W;I|@*?gUo& +o8k5|x  (w8уT+}B|?v,E@w\c,2cFT08  cp* s H\{s8`lo뾽46 Z,/juv/;t^7Pt"i">v/fP*XdU+[rn.\{_t-Ko̪ly|<=0VwDLnc6 9ue]J 2 LjO@]8G(|c`M ׀9輵5s/^0c~cx"CΕ!߹CćDsWɳU?%?_fݻ;$ᥙO@H}J*\t/;G۔DR;|`~S - pS*\d: ùƵyE34$r%"cŸCܘW=& |0@8a@JrxR,րek@qbXG1 1E$-C S+ט˝ʻzxȞ6ksdK߇<4M(r&߳oj ws>C +d@U2>:T_^?d{&vc'tXڿy׀@^j1l \?1E{ zoԁ,Nu eOd/bf_0% 1%A xAȣ *#aCXk0ߛ)%C?}n&w_!AN4]ko HI@l,jGŗw?p3JD iPoJZӷGEB(Q?} WNϒc@S < õ7-0ٻλ9sٙmvv옘8 RhE*P"E+B J$*Q!@C I\Hحډ۱]o쵽뙝۷jGs4Nb{^֣yY3:Z|J ""<@'=A0&Y*3>5D'SBP8A*q)fFXZ7pIcůk/lk4K k2'(eu,eJZ8GDDG-]#=2S#""" DDD p@ K0 pt>ɖ)O-Q}w/W ),e87[^ep a^[}[2l &sl1(E$rBOwҶy!@ K_O_zCsz6 Hwx 6pE@"""~cD`f~X.# 11]fp$:r03}c.'9rq*;-3ݗu30HlkeL[UgwOfr_{fE%CDD1Zj_~'MYUPDDDȵr$ `$,4w¸zY2Ƙmbo&[p\jUX%XIR\uz3:p037 ę<88Nrf-$Lذ[&)K9ӓCYn:A s,>6/޾G\c[jJ:ுWUAQ$"""Tx8>Gr1O@_c}," zfl@,fviV맛Z%.ԫQCq>40=>*Qm4=z Df|dj}-wMR:`]à0f>5P[_xi;5T@ s g]DžR<Ĥ{< 4TEQ$"""Ee 8mŒ G'0 ',L)FƋ%g93G޽pl]&33v'Q q Y,FW5hq`UR~gj6i! P 3l{(N(eW[.U{_= O" 2 Eqqޝr'fR:uŦ1h]h|1ߓ\n17zKD,:""^~E:rtthZ0fmEKu25];q4W]<3{jj+bXVכ zmz-b% aC581L?YHxY>O5~bS{8 "" 3}@tM9Y򉈈 YjSHb$!H3Fj828+1f3UԪ:/c-\```Çߥ h4قd~8AEnFF-TNׁ]""mWUI`N-'"""eaP-V=k*|?ӓ?"3V`]/ x mxd(+h5-[v`$J[(c͚inDl|Qs va?'ڂ33c`8ÖNYu Ǥp,px˵'1̓ny~ BDco=GDDDjuxm "pCнoQ(0܈38Sp js#I>]0C73AqȾf B)כ/wyo4 Fg{_B8{"6nglDܘ"\< p 2^M<ܧY""K%:UoU>p"""571;3>Ar_p40#壇EGLTt  p"#zͣ۷b7@o_f13U* 6(i6U@6!|>K6 7̞Lte2{(2l,?8HاɻӝљӮfv5;, |pD y OPUDDDD|T1߄S03R6i[oтiQ,1Q~cۦ&fap03C7ɥcm2C}K^W3\"snvep8`.lX 88pV n`sxUDDDDKGp@}典sc0EbɑYּx~['ߪ[2K>-h7Ʀʄhg]AL!#6@THxOG(;o#{' ySm3}<>%q""> DxLcQ$"""+!0Ȍp E1 Sm |V~򮻉HCh9 f^YY;?gYns? '(Eව}DDn\g",|"""HDDDnE1c$d]сx&';Ic@0_^7IL}|)@uoj O 7&P&E_Ψ|"""HDDDnu$YZ`=L @nzt9\Opί^KK^W^V&8H#GMk\UߙL[^.ŽH/ iJ@W.\. BmBIbfcn )N=?y8%x*6nf7&G<̋I,OrED+kP+:UBmUJ/2 ^Xzj߼[EAxޭw6Ƨt[kxfED<WXl/Ӽ7$YhHcjP%bBX3ՌbFV~_W QpCT!# ›x:;0"\۸K_DDD8=RʎRZPӾZ35~;9Lیڶu< ҳXqZ-6qn/``5;2".e^)""""Χ02gaAP*Io{0?ԶNvƃ?L֒hd:q~{{#_}y`)vTLš'7-xMW6xw("""`ۿo\W΋q;oJ+"ZT ( 6l$PQ  P%`P i'i^Hc~}T<36#̝3<::yt*fw[ aVS!6櫵[?ϩw^z.fWnόM󳳕JS"F3gf'3{wtjrSr~m}uPo4Z.VոOͮ'y`GS@aZ.DD= |Hp h+|"""( 49goZ9̨Td+\>c$7 E=*ӋťWY}xzfYg((306 "P]]\* ciq3^]Y(/ǝёtjrZ lcpm/&wYk$M`{;8k`2pB'"""J< p8ֿn 2chPL0`j+k.әJ6kq;R~q8.5/rO~sc ]5<39:M#wyfɵ:  ]2l6ByɥY_ݗNtfƻ,Yk@1p*NgHK<gGp@Q 8,1;~}]F_2z-f7S T^aHYd ft艣lן,-hũ1׬`Xs@LW  `di%u)-"{3s༒?"""ȧ \ejϟB/d9BTc N,yg$0n ~91y]1%%^^v+|"9xȒl7J|v3o໣CeF, Nuwicasx;dQ mslf9(a}9yC6;-JkAd뫩ldwwwZOlm8hu045i xxx9} x NMy#w`BQHDDD>Ś\fi p>|QA3mfz 낔FvWZh6} !#X=s. R~/Bjpa߷M䀊pbp :ei`\D<x$k4GL8VL s2os~9m;=hfފ_BeV%"LϊyF]@ \Vr$^)|"s/.?UGDDDD y8Ձ+50z[ ͙\%_M;0=R5vvB}q0}1K:(i`a+s]b\ i޷"[-L1U`'"#$ PSDDDDTNDDD~+8W;̞v,ys$18/tڵj+,ƾ{3ݳUa옐҈~@J!H ; ,@+1`0B M\7d9{I}pxy9}rg{?OYO_3WZd2g TaXu,nּ'~x/afg4Mq+xCl7x)Ն VMR*ʱrlZYcXX.m74@QebMF3 v \8k ,>LeEs8wX&'v]ͯ8m4mP@S]Ze}f{oRwC\5Î5'_D /"cjxHcYA2 4V)Q4TQkv_z&J.>a\x԰ǭ("""U4(te@W:ztU @ьZ2b:nY! -oS>P w {q[/"x}D>%""""REDD(k=H,'G@iÏ(ڿ;kTA̞b$^i m" 6?՟X`e') EN#ʢ1Kv'g.X,E"azycO)-x;34xIxӚL׀q'e)J=9o?&(JCR.MukѸٹ51ٺeִjSujˤnPsj (NE2_@ PQ_Ǐ,Hqw!z$}wsp?QMQ!0*,1@MHWg'XGyuT j=> wWWh{]Mdx :\a9rvw"US7.$ΈWr~ׁ]E@M@) X L%0Y@9ģw11Ng %%e\.*PDDDD[NZ ӌ1\ւwӬcj `|]X0Z3#;Sb@J("0xX`| $*gNjn.-k41R@"""2r8[_|b2@ε^g|L_WhQ /@6(ſOwT> P0,P[%O㕗c?ȚՋb~m_['""HDDDdົ{8z4k8d 1ރ@3|ۑ.Z@W=HoKi^OD|_!Å+;VXƍ>dx^WYYMgg}EGOd{o6Əa(c˖X^|q=V=o εk7k`޼2yrt7Av cLpmBnvv9v+UZL_ ,z P EWP804FyJ𮀾&7ukbʥL6ޖ-[GiXyںJ!""!"" $$ c)I>JFi.wKV)ئ !3LK'Eb>jkv'c|~ޢ*>HV3@pfy@ 0[?x ?Q MˌKٶoۂI[ϟ'"" ;q</_e1d^7nӷꢲ F#*jӧOGfbׂ *.Sjjhmz`OO/{Xpo=DŽpQ[D󴴴kь\m!X L` T"#f hQDz]dg'%%Ielj}u5[b֬zh"""sn799套±cgy :Kjqoz:;#''[-Z^,~c8'8q>eezBk-466./tN04Jau2?OR $i@Eƫ|"#}oT E^{z[nABgkƌ멧{|니 yPtLv=8B* z۹1! +:G.f'{lW\RMkq ¿ "`'2,V 8*%6o~ޢ6Xlvƿ_9Byi g<$?d_v6<3?/' ;6=[{&uǽ?3ol]O?$Nl@\䧒~0?fdB+ǵ!Il8%^#>({g, J( ʬ.pD-!C 6ݴV^ƶk{Dtgއ&Yॣa6m[34С%  DѺL$WkPc[G'a]u}q>nbl݇]>Fl4^\?_ Fp8qVs++F /H$Dkb+jzJ~oK]$V_P(,;[    q}IEJd.lnA lk;W VӏL Eeۛ 2LI&XlP ifJeC9n(x8n8م4q "JHB $o*PR3$څ)NXteB1*?VetFl^Xy:lljlxs9[*4Iڌ=R-rsQoV4*,,=|Ӽ)*-kTWmF˯APb⥪PMIYXSx{C$GE <H{=]Z}~]ph< |BYA^q莑{^|Nb, ht!wI>sH8a= %c\lx'NڿAyvL2i)*t=^~[.w,-} yTRRUVJƭtHjmN=3^8tMh|E}ET]]Oac6%si|~}x 4=|>X._˾ :]ûDΊPi)ى9q}o'?"yO>Bml_$۟(g* !dXl47w)=F&W#}fNZOձ9WT˝OvC_!$ L߳,;.9%K/,|}jQw}:vH<_h &u8E؞lAS,jEu&:r0l}+k 3iEӫ['(%@Y9籎@xHeR%( 㡛! ̠M-"\UD)) @тwe;!&pZHAq((~0+ u2GvsϹw3=b[xwtB"w-[# .`^49 A~_|~bvg] y{| 0+qyϡ>5qmzj319W 3"B!@G'](Gi^yH띮\@Ξ˾1:` } DmoXOնg~4>l1o@i8G{Ky7f< *q'yrs,zNR+:Ա4{u2LjblB!B@G&Sy*[FYe`A??ω)M26԰~it/]o/h/`j ނ5eP0 r GX8 5{bkV۞Pt ٢Gy.]"TUUV,_7J %Pm:e{{A\XXof5-Ar@N>yl\a<B!B"j|7pP.DЩ?|W/;7W쏜$M B~@MИ 1xXބrh}8m9E/J̬ eGrKKu]@{OHTQƉ"VJ"Edr)`4り4+ ILAcP6"12pr0l3ǁ//7f}?vs{8{N UUc0w9LFOGY{iB!B hHOr$>ɜ)3JxJϟЉ#m톆{5{z^jqfg<(b1/9w$kjc47?b} B!BDW$EI@RAF{~PL1!$,9]סC|r0-P‰6vlCC njq"9~Q|0223r,wZ۸IUZz>_hm--F08AB!Bhz:$Ւ ·dveU0=m Bt~֑[ %$UT\&m`FF^p$wCPR COHRP U jo__nCޓժDsRy2Z!E ̰قJȜ?~Sűت{70Oվ\llsjjP* ˉob$@]YYu\g1rO1|=mi)jXL"B!d䠬 VW-2/|dp2),%H6tŧGgm/TA@yh \KTQ j)4l BH"D!PfѢ](*-Jq# md[ ŒYo.(o]1wR@\”S 7b80p\הߐF(7(&NqIp e !B! )Mpݩ1 )bECnh _&!'es5hrz4z5戜m{Ls3by^7LiW{z7k &M:͜J'KՅ@˴mjZ,%66~q-;6w++_yqzG1mhg&H!B!dI7?tF#rN =odX}*AS9!b>h r*@},@40?.)B3Ǚe{PrDL ^{yKlXZ8(H%hvqSTz2T\Ҡf0b9bg8DdH(KC8Cfo ܬF~M3'JB!B@,s| />LJ&ZF&6(,2hrDawm\) (EMhrz݂.h3CS3UV<P`gPϽxyޙXUq<.Qk%4tK[mZ `BqطY :l(0,"A2lR0lP 5æSo|fΜ;wνÃo {}]y[ Zr] ,X=fuU@'Ό=m>5X~ A2y𖛛G8:7Yw{A%Ɇj4ʌ#Lij$I$I$IR* bT럗dW g>֯/,,|8l,#[cOse&U9s#*sX| #5ҀxԨIf},yrd-~[ϛ0-{8a,Pnܸ͙OIÇO J#D.m@;sm=l<%S1ic^& -5oB:ZcR5j7~5&I)w1xpuq :s(u&fӦ=o__\w$I$I$IR /((7M0Ү}YuQ,lnؒS?g) +Ҷs1hr#[|_KB~X;f " y -#]nQI@~?]j0b>r]䱈;|boD*cS &ֵ?9? 6n,] ʺΝ{1@,[n)rN>lS{$I$I$IDņ fvέ@W^㟡L/-,hgCDQyܦ,&~뭕NI4N6ZZj7FPj4۪tۨ5rYAOh&0ʽY?w>+%1{w 6[DZSN@ buĽo3f3رZc5;"((Y{.d͇f0h٭ܻ$$I$I$IR zZ@gD9/4iYhy_>Ͳ$ьE&Rr .e!csxy-Y_QV 2,ZeFV?(i-dP_5g5'41π(I@(g @s k 06 ׁebv12g9} +_kǾ)JXu  v{ukkw@x~:NOZMq(lCe9M{$K$+W6WH$I$I$)C sBk>$Y׭]ԭv0d/p|m8;Cs [?\ {63v3GI@\cDZJj Z9)zhxx< ~HyLi@~TvWk4h5o]{YCL}A f1i]_'Jqy>s0kD #+ L>e.$I$I$IR& d8ked ^5WΘ)xz&b(cQ"suud F"SoR0x'=O?Z@̺hԨ3\`&r̚ި% 輼uǰ?dOIJm1^ `DZ8h5vCւYVy9ԾCof%ynJou1@ crqy$I$I$IR px@ߥ()_Mă' ,\,+Uѧ@ ?<`IPz ? '-zRn gZ@6lBysQ:7ogԩscٲUN/蠟Vߙh x ~+NYeW<,3rD @aML HaRtaa1B^1WH@$I$I$IwfI*RћȺUkd~6F8TM+~}u'?xc(3JCoYxxzH)N?7ã}Z@U7{z;'f|Az m;#[/c9hiEhv18p6mZ98?پ]s9|b m,S F_Gf3][=(~'c) f;%KΜf#ccqfŊ5 a e k;`yt,ٙ\׽{^6A~ I$I$I$rJd7<#χ *A5%.bAJR:ӳi,wzhwPz??I)׆Gӭoj>taNd㰟N4+ ׭5Oǎtz`em9'he>y0ӿme MȂG=z @E_j} m7X3 I$I$I@aF8˿{ڴ׬{ 򲲌>c̬'6%N.%N]p_ `y v9޵*)92qV݌=D9<6WIw {5rw~eRo3O_fT2?K5{y JPv*Y,NOJd:E@\ğyy!,^;n*?׮݌Ova/90qRj w];q0`Dd@~&族5z$ pePbY,(Fx3m籞w.oʔ&?ٳ߬YS3C~,jz^U96f$i39ڵxNœ'1.,תՔ%ly۠tK0 .5Rp]͜9 2o9c73 ((>j=C6lcs &U'^$D[| & _@ I$I$IT@hj@HbĨGc_st܏τ,֠i p(ac q /@-5l<=~G'%|n k,eXH$I$I$)Pnn5eS =9 &^9׌vodA8X\-v nJRE* pJ .jZ`@pjh!^NCo<7G&2(n=/ u4 (/;aShO20J+CB@yc{J?ˏ(~gj_W|%!ju5Oߓe$ҢF@5@vW}h|Q2CJ]NW9! 7atF|S ojtV}D -n܌dm-ђ^Lah2kT7dM@{ ߢW?z˯o{PT:EQ B@u@=croM!xė@.򶶊V_xV|69hjU#RMR+=tflү< 9c 墲!! @U8ob 7QQ_^jO'JϿ7uW󬩉 ۺ z?@IߥDаrA:~+Jnd|iQ%]r@z#Y=}H^&<(c2Wtfra*tDAw/QpY%S/* ? L'{%5_Zwj|$A>xهVUq,*#- ?ILJ͌e-6en=0QI$!k.pVtc]v -#/޳ys·yM:UꟘ oW[[JI% eg$7{ !Y!tl,Lc@@+  h }ںun>Ъk󿺺. V;IJz.(?M366f}{(1_ϔIKcY&Y,yR)ɒLg@@< ]]KSX[csn*jH͝Īr%g{uRkuL%+j?FJJ\W$9!5Y2ߌ[sNQrrͥK_es"4ii9CeJ{?vk@.n9齔VjK arZEvTDŽ .^ۢc!<ȼG12K~*< 1!0o fJJ͂"]G_fddԋHS:u=1; WcǾ @Nzh**F%b'jj>ݟ螴s熴IG/z/ K99F̚5̦o]>09k|"$xy/r#izD!! / l9}Prr-&u}O,7.~8imA'{t={FsO k:;Ϛ~ $lI8G9OMIsQr'" &4]hUɺ7Ņڱڭd>kzz~rr*ʤaO*e]CUAǢI+Ve^ w,ֿ̰Q̙SXX26&IB W29?( %s9!Qd'oFQ>Af Ln:ϓi;rtwѿ&~J~nsZUWHMͶ3<*ZZVIJUCڳS~fsϬkX @` wn-g%NIVv/.jgd>;" E\kZך7?TTkl0"$essٻ3%˖tٻتEļ)=O]hhM_uc&5i5֞;kI,5lg4 卪_{˾}v?.Vٰ'Κ^Yݏ_e=NE-^5U~Mx7'3{Rlg2u^=3Ҫ'U 3I2oyWP}L} NxpNz2ʞN\<˩Tv˵mԚ 0?6 @ywY#BGZvR4N3oz"ʞIs9 @jtѢJ~>sȑ2 113GtKItsv){FLRtx?}. fПHN3J7Rho-0 ~:4haaږJe[۟%T*{(0阮X|ͷMxwҮT*[I~~z$ʞO 5\24o~72ѾcDoZRPZ\oҷL~5]:]Pvݏ?oϾ(_}ucOT*۞\m;ӵiwmG]i`j6޷ubЩѣ-&1RlKrǽT*{4rTg||[ @oJ$ޱrGuLҥ.H[Z~)Dǿ}ǼGeppO9L jGi]*Nפ6YO5Tl8ݔ0I21Ƃx*=ou+T*'y`dt`*mJmpөTd:+ $2vti8F3o17mLiiJ$D+}T*ۙ2<H0Jҹ]2ɏ{ {`db"iYڙJei=.H[Sl[ZH>5i$Ӝ>v?$3m:3mJe-=ti(+$3yz6Kg׽(mOi'L e+))隴;FӺ44|ҖT*{!-vf;h* s ںx^~y߬Klϫxc׫@u>_?hy@y Z^kklV~^_?_~]o_JoⳀ΄ޱ# ?ɾe}IJ73K hnf|;gUs~vc&-޿p)K]O*ueLל'E\w:do_x~(WC` yqB%oes-tU&^+c0e7~89*y6nXWe eNQGk 醹n3HU[p0+5!bb\ 6\vx_#t41Lr|{Oy")PH E"FXB)JC9KJ,RJ-jZj5| cjV[k3igη;>#4(6|fiYf5; XyUW[}M)λR;ēN>N̚3OZYsY7S|f1$匌xQ(hbFSltE2)9)cd0nq/y31?2g#sFC~Y[biR*6~@ t{wqWgba7v=>:jqgznKsk^%):_,l3/Nl8%S>x܄?QlZ v} $&n+^ŅnOfj;PHwt>2>fF7vEҳap@]gn\g9SPUn%];Dky[`U)Q;<~ ˧:qm%V٥$T2.0ȸKrp9;;W?562ʎMHfzuc%4{ f()!7]^.'Ȍ:s#]e!uvgz)aN(`h"jN{d e5Ȍ͑'Y2q{vXyE;ٳr6c3Fî~5سUx DâcJ.!h0sHۘxfI:f p.0d"i%$w-0{<(2D ^ z`.{k5lH$ۆ3\ KF) @HZ]9m9vH=_ `;|[&P=ŕ+-dFp@(`SFZ9P8Ps P` sNu v,W P1tob:`6b8,? pET$ި]H4ŃV1šI-5y)A6d>p]ZhęA>szbݎ[qW#mtIg<0n"+iHK!6ts>=Cr}%cJ&? s!+IGs#p@ï`i2ib/Vt38J>p/O(&6D|:=a*[-7н!tS" t"tQgEQnWJF_tYͫOI![ k{DPBfHA8N yD$PŷA<٨7rSI}Hz^D}jo}A{V' A7m0*-c !#d6Sx)}$Uz!nWv0<) ,hN|!7Cۆ}<-tIFa.`(AԒX- \) ;"x#MR@ N*`F9k6B!f՛z[R ˆHjGmXlYA?~bRPL\yd+ }Dkq-euuA3=(a#b3jG*N '$bn=큪Hـ0.3 tFq ,D! pHsLy|x` s>c=]WD܅l Ξ~ =:-bg< .a"B; ј^4H'̆q)i"IT u߷Y .JqG8Mˡu8e SCŕt֖a F;8(bjqؘL,4l& 9X WU@.G~^MgXŠ_`! |vү­Q̗~~ ڠKujl@Ɵi/c@0mi&R,-U!4 nJ4}#)Ift 6e*@e8QRH̗L}bCv7*YA [CJ1 u>lDG+|p<+"Fҍ5! Hm@)+0pF%Ft(Bx0d;zbZrE]PGرY (DLqBo nF-X#t.k.ag]D Ǫa4—dK-O\NxWiUθLhKS `]1&C/lQjjPY8z Q3qH/p++M9SU_)9LjR?ueUf$ ^.lo%}+*Z7-v61ޡ}.0 oR#@_k *{ hfJf{qMyJ4«MSէ:VBնw27]& SM$* ـ TSXT:wQ|*`<Rܰ,nҀܕ: <#I0~DYF\M%qbI'0&~7f&e&P)sXBC$$| 'Hbf'%fF 'o.ʒ>!yEa(i@. TYJܧAQ9s'^@|Q}i6iGd<8/&|rm c2l(O' qc/fjO'q9b xJ:ZgeCޥ,/ZD3u^} +MIhǺԈb H?q10d/$kğ"[[X?I$ʽPTR%C#Q,t tW@')\=D˸tl%4 rjG]"RQYŝNK#YJ(dCyjk55)Չ0+& ^0ºb>(|OG; *LeG=1B `!ۈ0ٮ^*3  'A2.- 4VD:<n#`߉[+m yzSw6'.m(3`\~riT2H+l$d Qr Ƒ_k<Ϛ :tJzpݽz9lPp/y#zmktrH~`Ȳ42 nh$ Rdm'Q (JN,=YnZ9La`*t5'};\uz(P+ E[QFV}=v%K[yg69g%k89MAΔY+K#['+ l q4*uY V 9aq(s*2T hzt٫&x5 0Æh׏$ByR V]6H?=W=<fS?qjU]%0KO0< OaXyS8[eS.:ngNՅö |bTԶK(E­)zd.p V:1mfJwn#ÌF3%N6`e(n&c7RF{xMwќ1-0LB5YjCzsnFu[7i 1bw=,x`0|} #7mi%LGa[ͽ10֎@jB30,{oim: rўRXo}7X稾cߕ=r|Յz;Mq( T7{ u??g$=#C3C alKZ 0uވG(ZSFX'þ_ @ S.fm% KNݎ THA9M)x52 ʼnc!9ѝ5t0m%^wG*HjJ_Hn}EB,Ţ3iuR3[D=U`d<džzќA!\z 0U]֑sw?1aHUg1tl}?G'8H|} ٟCU(tNǼ"K). ]ꬖCsu嬡Pp9'o\K %^9: H:'I./@ /Z[bvzz솆,N9u_He%Owj0\)E3^}<)[D߬~Dmlf~-ٖkQ˺&V7>'6'W#A WW QYUC \j)Cg%Sscޮäca\40ٛgjͿ3/YJ#5OPƚ{dqJЃDà'zwk_T&WMfzTXtRaw profile type iptcx=1PCwN%?dž aNZD\_@LEՁ9Ao:E/]`MD?t PQwi iTXtXML:com.adobe.xmp iCCPICC profile(}=HPO[T ␡:Yq*BZu0y4iHR\ׂ?Ug]\AIEJ/)}TkP5H'B6*_Bj!(๾]gys*y>xEA #ymeTHbK!@F eT`!NF4'}G r1 3F{[F $Idž$$AETuZ[:ٖ׮WP0˳ Q/)%Fس(qt/D  Ylm=HUW]*z YD38ӮPVTtt$Po)B'׺ZdW/]^Ⴝz,'TLC9I"PA (Pb M bAuI ,kCS[Q(.-x=r78IgWN#R`SĤ!%eˎK9_УFbQooA@QKy.![DކĂ" =Hȶ) z EWs|Zg 9"I'V)=IG05L!YP1_uHʙٓrfEDzpfv`GҮq4q5q#! }z!H,@|bA)P$(Yā-ޓópQGaT>9$˗'Yh'P PSAKAG$5D'I7ee.T8(BGO %!tuv:::z2BGPS^KMj+lEeX!e^X:: j1e>z4@K8iGӝ8H{+%Q]tXG+Hy]Җ! AgS{@-C$$36"= D0뺞).8lUDPУ# / Ty!9([pg;ˍ۩Lœ]O&$p;cDřb!Уޙ)cjT6Vik:1[{%3  "!7iD  9-g:3g %'lYztX01H'!];E.iP;kPU+2ÍQn4%R~CڃgcPݎ.Kf# pl!ϔiB* |7w X0,> oqބ= ]]~G:7;[j=%Ԕ@ d =!zj$3" h@" ar:$BxAA/[-0'*Mj%`á9%4ZCV?!l(@R-> ibO-l EgK =OB! lͦJd = ?BD &򭵐yYK*͆HH,XaF^̞5X0\Sl+4,bd E$'X L#&^AB&X A\(PBzAMd)%zUPQv$(f Cex50FQӝ YM&g5J_?K(zr#N|(|1%.A`l !s\k#%)R"grԃBH O}/%O li(g|>b} -Ig(F(r;)v2kTlDpF$iH*rᖂyPBXNⷧH#D+.j]T"ƞ@w } ߱g`ٯX˚ U,ci+Y2`y1{{NM {e DC+9 ;?'U^5.4::'NCpz *h(D}k֘N2tRUʫ鈒x(Kld,JKgΘFRŦu(raV@wv1G+gDMHk9RУ|pi9ceV ;  5FM~JzQ$`5ڳ83+Ԯ4<㷵 m!dCd{\Ջ{Abbg 324!Ib]tЏBAmQ zDv>U} Mmlvf,f%T9)IvѠ5ȇ'= rlЧ4%tlnO쪊׮'GivĢ%UN|>Ūtin JfSZe`:m -4{Y5gM1%iNA^ƭ&lޙzh1Zwj ;L35"gZ,J}kHp8=< x(B7qSA}45ᡶGqh"lHq)cr-N'jy:=G2I#8jQ #As[G { <K:u IMCS]%Ѵ,$G{FUUy\T{A=} Dvv/ ̖z_4~ώbF $(-gd  [84#fԭb3vlfbsq DN3l`1%uhUʜ6;9~PG/A g˞f+"&V:ʹ (K(d[`%XY ]$AZj4p&{@B)L,Íg8Twn:HĂkΒd" ]f42:I-h$$xjwq9!#؜j]Ěr;"=%+16wDH8 =NLÐ 7+kF=ŌԬQDubI()/fW=IT fPw(1UNT=Av=.c}Nvap;e{SNMtAmLlltvA>|UZH%JUe hFkN4#0gF iH>ЍOAŐJxW%:z, axЈESEQ^^DG-]Ϧ+*"KhE($|/ga:%%.흴z)G=;XBE/ xgIz!thnUd[tz[jdže!t-fGKRRV b!64k5:1FMpgp w'SP  gH$ŋYn]N0vX=\nwau!oͅ>vOvdO  5yᇳL֭[?l6V,bsŖw/bOv X |%ay|{7_׸gꪫhiiaΜ9g^z)?ܯ_e˖W.Uꠍ+W/_N,qqGf~0o<6nȱˈ#Xt)sΥ#k؏<7o _\pb㏳k.:(?~mկ~şg"'|2~wy'477,c IDAT3l0hhh0fvO> =644p뭷0uTf̘իܹ {nFosĉ7MWk}o{ҢEӟi|__|mL6Æ `ڵ5j'x"<cƍG8__y/fqwr /(7T֬YÈ#6¦M0aB BL2뮻|;vi477W_}M8#۹8쳻e=ӹ曹z%kL 7\-܂f'L-^]ױl5 Dӧ0^z;WjE6z\䦛n?1Pn~Y2 |ɜ ۷oOۿrʬ"![\u.{P U{hAB ì^:M x≦ ) cc1ϟ;w.O=3gL wڵf }يq=4p[[[裏9r9f1uT`oo!6oLkk+vbyC(?>k_rknn6{+ 駟pGC2gu;vt邏NJ;nӦM@YYiWVV0r{1{lfϞͬY_:nrٻD̸0iPUD" 7qgVLokɦzf 2ԛYPAl0`v+4bMpal2bcp\lz*`, s˖-foaN#jnt+cS;;;P\\ ` `˖-TUU%Kyf6nܘW(B!^H@ tXH$ $\sMilldyŝ1$n3dN?nb "ޓfik{7IӴn幬t }p8H&2 |Duq(KJJL93}Q-=WZ4 hٺtA;k#jp$h#.cJ2vdOzG 1Hc<ُ_ !Dp:iرc袋x饗8z΀! kkkxX1۞Q^|i> ]tGq|NkD/ԑ9*(i|o| ~.WVstRbd4GQJ):p:UzvlΝMu ]wptmvAlGs3 ;G0|n-#Gd„ |K_"HhѢ^ĉ!E f{l6ƌCcc#WfС3dJJJx뭷F9{F twqwqFV]cda7o(7K,I;nԨQl6UUUwqL24֭?q\//]Mp:3&|r3.ztK_(B!^{5t]7αX_~HBՖ6kәsvӖaݜt]ˀ#R=M :*#RsՓ HgٲeqJJJңFl?ַw7ZZZD"^<̬׸袋Xt)+V`ʕL2{;裙Bu)q[ㆍq>֯ 7§l^Ʀ5j h`QR6QMSt]W4MS^PR Bٳ={P\\l:橧bҤI̝;W"I! <tVVVƅ^+G~/nCHUUEEIP%(JDQNEQ(4(Ҡ(J(@)Ì%TW8j=A7UՎ2k*oqZ4+tYCAw=^ל_@YA. W_ŋaH}(›oɒ%Kp:=;` A+n /:s3S0`voBAz AOEκj PK ^U^   M(H   >6(   BAAA   PAAA   Zkkĸ   PH\b\AAؘ:Wouiiߚ}uAA0 UUU l[0BMAAqfAAA   "AAA   AAAD(   BAAA   PAAA   "AAA   g |sH$u]m  ( vEQ>{HAA[t]G4au߇P4MR  pmퟗISSb1NMEEl6I  y lڴ@ ]va:t("AA u 6cǎO&Q__QGEqqAAsgrzTMXjU"J$>YR   BڵkiiiD>>K/K/K.K.b ^}Uz衬~;\+2sJ/cu۟L&YdAtm۶__v{/?%-[k<+NV:::Khmm5[/6s'|ٳپ};]wg ,/eP嫳bߟyg\׿5=\qXbҗ2T!Ν;( >~a??ओN .0{y]p!2ؾ}~C=L>|8vqbjkkODAIII}'NF٣PXreNoOgMG.scݧe}Q~VL($Ilْ3j(Ytiu0;v`ذaX~=eeexTVVr7_R}Q6n܈i=+әΎ;xhiijoOS8{!Lr 7PYYɇ~ȢEPU/| 9㩷1k,~<ʕ<3$ +)//'ca***͚F,X@0d޼yr-{<$IJKKwˠA+t]cɒ%hg׾s۹kϹu]O~ ;o~{1\.hllڼݻyikkv=^x_ǴisWSSØ1chllꫯ&h"6mڄfON˚N}}=cƌGU՜o~4/|+_tvvORWW(tI|_g֭< :~ӟ?X,n6'O|EQ9N exxX`Axb~mEa„ \r%K/D]]X^{-~_ 2\q5*k<v+wٞ$HϭL45k֘Ț5k8SO8餓}Fl,^$IJ:꨼pBFƍinnLV:::xٵk3sL8n<|֭[ rWz9׿￟Ƭy}l۶jfϞOV+m6۷osik{n?|/,5M9no޼'|`0`֬Y?_˹\bܸqL>vq73i$n#<)c5z:\uG} /@<GalܸGy/ŋcs1Y?~?f̘|!Ws=я~Ĝ9sȯ~+~ {⋹կ~g%2n8JGֺ>Njt\x,[={p)}*G0`׮]bx^qm۶>#?||A ěo+hoog,\2)x:￟)SOi?_Wٳy衇@UUv)S;w.Çwg;ߦ:;;yǙ3gs/KWRUVX_mGoi<̚5S\\L8_G_okk㡇keL0E]ݤ,XY[/ѣGs-o[nFϟ~n勷e˖sw3o<^y6nܘV1-\o~;6o_~9wD;w׿oyk.k]s5_2s=̟?[o_|m۶+?яOYfMx~饗8ꨣX`c֭躞2ygz׿K.eժUv2eJ7`Η_~9 ,key/3?+VSʗVXҥK/Ikk+oFB^UYz5w/~#?h^~= ,`9!Cp}qwV.8q7cvbƌfB$a˖-fnkk{;ό|*17x#cY˃^xR.\O~zꩴaQwi&nfNv{gN}QNg< j*.b|A `>K!yWUU>Cf͚żyOY|9sŋ{| OpgpB=\x~-Ǜa[#Ga6l  Jꬾs}'MYgԩS3gN&L'șgɷafB\!_c_ibo)S[[[y'я~ĽK(_Yw{|嚪477s7ӟh46GdznX-iL ƂD24[ZzZ!/:zh QTk.VZE"0֖f5r̙\466̉'!C3jƌtRZZj>o!l}7n{I{NuP__O45[/( GqG2tPf̘ʕ+4k2uTl6^ac1cƌa|_d*|rJN8N'EEE}݌96mĸq̰ؽ{7z*#G7vZƎKYYӺu6m6r>4OGQVMt2l0ZZZsYY17ovsק5겞>})NG}d Lݚ.]JCCÇ+M6UVid+WvVb N>d^/rꩧlٲ9fJJJ2eEaf+Yx|x444k!ﵐ/|̘1ttt 9r$fa DiiF:=nGUU /d֬YTUUQYY^:PUQGf$O:EQz,njc#'xbZ=]|JUU&M2ߏ#W/$͝;l1\t |Ōl "o:/\_AfM-?^{ۘ:j(PU뮻3ghs|8r,GB N,Y*Y, ǓY33K.^zziӦq饗rc fݎng(JaN CUմ4UU4 VqK/ Z =gggg};δk2zXn_y" X|>_8EEEݞ-=sx<>D.dKt0qD l?E{n~xoFsƑ5Y~@{ЍD" 'c^1]GGGx>QU{=s^7g:T6xb<^z)&M|𪫫`Æ $I O+r@ +x4eÙX߃! B⡸ؼoÁL ZHyҗ|n;vdqƱvZvܙf*{{O~3OyfKe.0Xcz|y0ş^+f{'\g|b ^{5$dҌ*jb`7o~_|Mؼy3W^yeOcK9ח:p -凞koijOO6Gog1վ"AbXaf"޳gOP\D"LӦMcڴiA~x կىiJ"`Ϟ=퇮}wez|>~ rUH$B40 ,"J$i=F/--eĉYgGȬx %%%[.RF>_f4b..&Nh溗\頪H$‡~ȨQw/--vsv믿.xҺӭsYY% IDAT_=ÇS6s9s9zNƎcxf+5X+LJKK˸Xj??p۾}{?x>x֞\顴s=Gd3`0ҴOx躎($ bXZ8ĉٰa\|ŦPX~=۷oO. \1bDZ##<winnnrt;n8>Ct]0d/_Nyyyȗ~wܙK9/Blet5G_GfD"֯_k#_gfQO F m?]zTTTtkTVV3ϻ tY4440f̘~N*++M_WUSSng9[l{堾sNI&qFUiw%,[D"AGGG.XN(?OƌӭBo'Ofҥa wqw3 63g裏);W^yN8p oV7fҥxD"y䑬[V4MK+G4>c97x]I$d֭|iǃi)c9fl2&Oc0w\04hvooڴi|ǬZ+=s1,Y4R|͜V{/pHx= GP >W"gp7@pI'#p 7( Gq3f̳g駟f\}(u]O}:y{ o~<~&Ol|UW]=C,vӧOg\~M9r$mmm|+`…$ ~?W_}uuuugGi8묳Xljδ/[oe #<{.8ٳgsϡ( 3gv3|pN?t~ӟx1c9ؗe֮] 7!C8V?wtM$ILb3g''4&8ޓ .'xn EQ1c&Lr1c#LrꪫPU5:TTTtԩYtzv뭷H$8p W]uU{=ꨣώ;dY[n5?3Y՛m>_%pСDQFa~d2ImmmA=؂37X`^SO=O?̝;[رc;hkk㨣U4)ShѢ5WWr2yd~PTTķm6n]wŭ;*og^(  "A] XPX㪫kC4R}bayH7@J 77ͽ3?BҤ}߯W^ɽΜ339ߙs*b@5R x {]T_!לEdޟg>;9gػ{rG0;En;t!>>3fK[#!d̙RSSƍůb}d.e"\tt aճf- !'I!<#WG V#E~$$$z:C0%QUUBCC"qg41LյĴՑJTZZ??2xfW!\~As"P9pTDy8 :~!nUR谞񝉎n5Pl7Z6}a˖--.3D ogǩ#ΌFCr:[7`2h^7#WY;w壢Z7!U&B!' jA<*+=Ъ+q@dW0%jcFtP=zuf v/44h9JB!qR{ZE1` @0m,Vs'WڈUpR+c '+ZHэ(,Dv&`{* [|gܙR+LE[aaatڕzB!l{4RSGz%cT(=RHEmmWpWTaJKEǙevPO־*l}R̬G%&BtXV ~~k]"ufls:%$$v= !55$GB!8{SvQVe!4ȏNU_DTB!N-ˋ+~E1G@Bҭ[7v{PRRR$HB!gNt(E8\ށ.#j4bh`0z4:n7Z2Ϋ)Vcwqqq\r%tرw,Bh{GoJzWZ… Lߙvojy4^c͙3{ݰOzۇ{=mRPP޽{ϋcuʕB!~mlǪN$C^s"Ӯuhn79n4FcHW A2xC/Ic> &LRRݻwJ\.a2ZWڟ(SywOjoP>jkF||<O{LrA>U;3??9>۲e uuu$%%k+Hsv!^SgWUgA6e7)Զ{%%&CYq9B9J3*(4ӹ[T> 2UU #,,Wyp8L&?읾l26l؀hdҤI$''i˗/k׮L0???? /`_{5v;> <m\\.L&NJxx8&MnO?e޼ydv;&1cs6ŋ)//j2n8zAnn.׿HMMeϞ=8&LU(((W^L0ͫJVVѣG&OlnWfggꫯҩS'JKKyGزe .n(&OLhh^xs\,X8dm?OǎYn&M"((]yw0|̙Sy|w5k~nxs\,\0,X!C2dHn:>cn7L2{ϒKTTӦM96\>ܹsҥ YYYy}^cȐ!tԩm|SPP@RRӧOGUvo+V7x}Xf o`6 "//ɓ'p8Xx1jr]wBuu5:`0pWsW6ϗ;|0 .Hn8tPk^RRSNm6v+V`(B޽;1MңjJMMڎ8(Anh_=8ȟlv6E-9Y9jڵúj $Knm3dɒ%\s5̝;QFdb:vHFFFb۶m'`Ί+ZΤI0̚5IELUUmĉ?I.]裏*^}Uf3㪫gڴiTVVϟСCy0anTUäcҥK6mO<ׯL***3gsf|5#cr[neرKxO{՞;v`ƌkvPϗus3o22C=AxǸKիGF ԿJlСCZ(°aؾ})#)){g,--,ﴁ( EEEx_߱cG:t?jzWJ>|8z@=%%%a61̈́PQQf#//[v;vw櫮 ޽N#زeKMN~к*_EQHMM>Ai~6{-0k^Kdذa*ÇgƍͦDK!UFɂ3#335kxx<>y۰L&f3N``w hj???O:v嗰400NDDDm`۩cƌjkkX,>wz7'RUMHJJ;dʕ }O%n{otGCoiϖ : 0f6m~&8# $l\L&+W䧟~aʧ=jtkުvoWś֎{+VOmӒe69Xkl6'ҵkW&NHUUU0`˖-rɐ!CM_󤡼v;/v.6[Zg;FȡCZ楧7~Nff&_|P!$$hy ! R^y|I:t@II wOMuEvr $88اBTYYp,vZUU*TUUt«l) BBBZM}СS;x`/kmw6B>}Nhӵ󫮮6˷qOd?[l޼#GЧOf'")ƦMxG `>5^lrܵV>MWsE-'s5wt|0~xƏ֭[7o)QKڳgO{Nv %$$vo?sc> k׮ܹM61eʔfWYY} Gdd$6ӧf~5fK\n7GmRCBB5j#Gl3=-trB 渾 >eu8V+QQQhիu4Yݰa:t 00Z4Mcڵw8r?w[b%i޽&!6lgϞM扊"<<ܻ\ #DEE{]K'Tqušܯ|m,%%, NK.m2_;P)k萺a:vf~V2do uƍqTVV6ģr"""[oe={P\\qZ']PÇ+oI[5-7fz|f4'++sz;Ƣ  ov7[ׯ_~%?ۑvdgg+[.<<_|of׼߿;ꫯZjh' yptԉT~;駟fܸq޶.ӟ<?ٓ믿ѣGdIMM%$$MwL>xDۗ7xÇiӚT{,Y?Ĵiѣ-iӦxb/_(9򄆜:t( wеkWF|m?O \.V+ǏoT!ڷo~d<]reM7 g\\.}_ [oeܹ{믿qZ']Peڵm5;{b0bz)<?OJjj*/"%%%L:'x4isvwfd̙(Bhh('N|G}Mlllk׮]2⼡6i1HASQu `u]u= u=OakU4FM )~4UVJqVZő#G#ݯ(77_|gҳG} i B#ٖ+UUQUU?UUQEWUU?[Sţ[QZEQjERŮ(J)P*R(JϥP Eq@t=\o#.r1b B!8)RS^^=CEE]wdB!NQ4:8sz#Ƃ 0BqJ䉂B!B!B!D;ݻٝ]̹8tB!L<$:wg4vu]FB!%EQPӁ¹&I.!B!~eҶG!B!B!B!B! B!B B!(!B!$PB!BH B!@A!B!B!a,**Bu>7{M|kiߍ+B!8v  4q!q{3Lu,JJJb|{>8̜n wod_y0i=L=K/=9s&EEETVVpBQaÆ1zhrrr?>iiiڵ ɓ'_޽*Y7A( !T\\u}DuILL[AAAOwSZZsO?I~i}Xz?߭bM;/-|m1?|`4qӘ[5#G~ݷMEUػg73(|Ur>}޻\ 871󯏠 ٺ9/>_ER^J{tfQN'ϦsB:t`0PpS|^ZӭGR f+}{{&l;}Mq?>ny 1olRٺ9(I!0t!++gZ]]ybv> ͍ZRneFyin%b?'q򒓓ټy3UUU>>|~oenc;-l`ҥKOζ>} Bбrs9) CB+'>(uh "\.I=;c~;wlAXPUAC nΤTt {CnNvy~i*]u;l6tlcb(fE>}1 JdT4唕Rt(SxC8́}؍`EAmGKy;3t(B@@}s͏s8u] Wm-0 ݽ^z>)|* ʆ`4>hHp\<0)-)!,2` U7zu,]ՏAC.a߰r|{Π!G'|2Ř[l63Ky 4Md23M&ً?xBB;7Pgr2'\wÍ՗sN*>|>JEbס^v9/>,VC.ÇZbZ~ۗeK_kϳOM܇ظm5)7o.YLEy5[n񸱅1;w7_'stO w4jMؽ;G2ʜBGtt4xPRSS?/KQ*ǚDbfiZ}gM;S%(n !B!ff!B! B!B B!(!BqU!eyx4 L&#f W]uunJ**OiQY!>.Z2]!g6P0s\˟4荃MG5o-ą$l&.*If0B;ZBǍsrx4ܼ“t]ѣLll,Ha !:dp뭷b:vYQ;IMM=-O8rSLiq|իIo믿+hv୷(N$JhV6珋 'h Ҩi' )//uyAAA\z$$$ȉ*B ΖaÆ <̛7`04;k|ff&uuu'(βe˚ <O>$~~~<#İ~^~eio8kjq RuYM~sf͚5>ݫ?gذa$''I*B &СCbdffl2n7s=駟rA߿?W^y%ͣ MӸ+馛ϽOnoh42n8L&˗/h4t:;v,|k׮EQRRR?~<&g}]?S\\LLL _x穨`ƌ|zɓ=_Czϕ+WK]]x<fΜIAAAiIum-teD)YЩmWt:}/???V^mJ~zbcc T!{a0(++### RRRXh7d̙q|'׏3{l8YҥK$ ,^3f0k,֮]KYYYk-?UUeƍ7~D֬Y$ZSSBU/=Qf;2ҩm|vmԩS':vtJ4o.'B!$P8n7֭=zm6zAΝꪫشiEQڵ+XCCCپ};YYYqP֯_OAA [ظq#Ç' #ذaw1 :t@IIIp8 i͛+gl۶ @W^f, 6vZ~{SNmߓwt &N@`:w+((lX/q}ב/'B!0mڴ(MUر#> vvSZviQF* ,n3j(zL>˗ &L@~|lڴ?ܸoZ+UmV+** ~ $''[m`00iVK{k+?Otguuu>>|8޼oi^!BH ~% gHIIlsoo|{&PCBB8q"'Nd̝;%K47$8YdddPUU >|uqn>}"N$?OdUWW? pPNB{=)??"2ji^!B\9o߾ݻc߾},^yΝ˖-[v n8V@JJ SL+ݻw%TUz!~m}Y*++[na~{(W_MJJ Mq^7 IDAT&餧vuI *B P0)E "J˝}|?deaB!@As=H8ŧbm<|c&R,#S;G)prϒuǓ t]ѣLll,Ha !K:3,XpaWvOy= {DQYS|㣏>ooO>᭷"''GN,!BHp={6W_egTVV* 7<!PHHH੧VXh}~2233ٳg\s Ǐ?#yH)ŧã{Go?7egggy$4VYYή]B! MKnBzCK.{+˖-v=Chh(+W$77: x<̜9HnʢEPU/զ(ooسg={l2`LXXf͢y̝;Yf1{l?ǏgѢE̟?]ywXnqr 7::7nDuz͔)Sصk˗/h4t: ȑ#L2J.\Hvv60l0FMNNg޽2&NHjjj- /sD}%$c$$_\~zjt]Gu֯_Oll,r !9yp>FwF# go!`0PVVFFFwhѢWU6nȸqxILLd͚5x<^~e&MDFFAAATWW]wŒ%KuwQ?̚e&e!Q@qC**"brUVވWqt]!H :Ifs~ [^#s<9gYx"/ Aaa!Ǐ'**gcp}ܹsl6m_|ܹs3g˖--[}v?QXXHii)| L2o1/^LLL ?<Y~=[nEUU=]woVW=~/c&c}\b&,oR"Ƞo߾ddd`04Mcǎ%B! }ĉ9s&?| ۷o'''L.R6oL(>^^^q߾} ~F}>;(V^_|A^^iii\p׹eƍbf?0j(fϞld2CMM)ںu+\r ь=:> Ef,YO>`FGJIIr5jWKXuLԸ.<1p:YZ!BTOh7m I*R0ܹsٴi:EEEСCٽ{w III ,8eF>sV\wg͚5 #2h 0 5̈#X~=nV}Q***hii!33ٱc^og`0SAAVM618~[*>PcC&t)}VVq$&&~ !BtE!\ImJ-h~\-V3d7O>,]_|EQ3y̙3ߏje ~_k88*^}U;xg$%%E8묳hhheZ<ƌCii)fBuLBnn.?n. - } W_MAAv=bСCk%HB!DXk8Z]^JUzOHNM!&ř㙹p= lYkQy:n1si\ƢJP(3/zid2QPP@AA`]1L& ! FcM T;߷?ttM#XFM0$M?\ahjh)m[ںb>.c4] %ZM!PLVo{Rݒp/Qy!BPWՒ@Jz|{ԠpZ  ZKЂC2/ف3%ۂ^nJ Mad5N+Vo{e6Ő!}Yi)h!B|;B~ٓ@)L`۾p_]KkN&ժum+!zt;@ C!@B!BH B!@A!B!B!B!B! B!B B!(!B!$PB!B@!POEi5>] I!B3Y|7]G%B! ˪qf`QAkLFL& SDbou1UdQ::GĢkh&-B!Do ZIqչ0'l>@y[dbFlȄ t>kh \ !ZJ[!B( J~݇o!:I E$6ťХnDfvh&%JJ[!B()P-Dř4 ]S1EKB!Y f=Bx,+otw-g#uu#Hi !BKthQ`OQUC&(3cF,-wDu>lUMSAt]CWB!-BtZG:bsAOԚC1 L2`qkz$B!Do"+3 !B!$PB!Bt(E zH( f(((E Y!(ћ64i:+vz|6VqRB!@A@E5lLF&@@ H}sKΡi-n^SЅB! f3ɉ B-z !M@Ei SSSCcc#v4E7S!a2eee<vmzK{f֬Yw/^@!(̜9s=z4/ ,G?s̡]YhQ8 /@||7z=6l+`ǎ~y[la׮]s{}y}3w%=x|]LlٲpK±)**B!z! R]]9眃Pw#::9s̬Yxᇹ{oٸq#/2d~'1b1W]ucǎ`,Yg}kZv-f͢>qƵWu%K~z4M+?aı<}۷" t:9p/. ԩS)((%HKK㣏>W_bSSSCJ/S\\`+`ҤIXڵT DMM կN7o#F6f̘e˨d̘1#\_h`fΜI||0aQQQX]o_\qÆ }B!Ecd42j(?nݺ״4 ~;Fs璔>^zGy_|6-[… IMM?1ǏgÆ 6m:*//h4„ Xvm1_|seΜ9,[>~S2h TU>cڴi̞=rJ4McܹL<YfQXXbȐ!>SL9eQKl4gF ץtEEEǵHۗpy;v!BD %Pw}u*͡8r;hZj3gr'/\1 {\yWXAee%/7n۶mP(Ė-[8w͚5L0<***hjj7łfO ?G8ro߾SSSC]]]8P9Ryܵk \z.9 <8z?3&O(l6ƌç~JQQg}6(\>櫯bر 1bDƒ @JJ  l6cXp8455}vrrrK/ë́BGw@xvn f=\b.ضX,lܸ?`02JeB!DǮG~\58*CuDe9h!:Ip)+:VZZ &9pU1|^&#` >j7*tӱA]ee%A !tBS F*ɧ9Iӟ 55uotiF 1w\6m:EEE`  #ٽ{7B!򗿰~ptrw/>1cؽ{77n{Ě5k8s#׏68#X~=nV}Q**[m+3)j)))$%%G*4h?}aժU'  c {%77;wH(b͚5cȧ~i׳yN몡C{pW,Xq!"9(MB^9,]J$&&@,;;[~BKzTB3IjѤUd#Fbb41:}4u}wmĄ| O>,]_|EQ3St1-EQ5k/ofYf42իW,Ycu]B!FEVVF/xh&M_r%|W̜9}r[JNߑ3;39sZL>Fɳ>K}}=w}+V 67 .݅G*ڵ@!'';v[lXbE44lB!8(NfT\^KrjܕjT3˙`h:tSRRm)Hi,u,}R&i:U4FM V *ɉ>@=/ YYY\~gt9ttR|>7|s&Z<']D*j{6@l1$9]>vph;2eU!χaƍw;0*P.UQSO jP$@xv{CrJ BAD>g NFViKaH!ɮkzFz7Xf /"f+:9m۶_UU;vqc3S8PQ`J6&#fs~0t7R Yi)ud@ IDAT c:]Bq [qM7qM7IA>pxƌᕔ{Ꚛiu{ipJ8V˪햄~B! B|qFYq{x|>`ZNMQW~XZB!bj6) !BjB!B!B! B!B B!(!B!$PB!BH B!@A!Ba ];ȶ*G?ȶilkXB!Bo7Pp:]Nwl0p$@8џB!wGB!B B!(!B!$PB!BH B!@A!B!B!B!B! B!B,7y][. oY5Ki !Bq xu_'-66RІ'Q~Qf1 ΈgDvy)d!B|;Ʊ4uvtC4ə$Y{(ihV237EfZZ/(8j'[Khr{WD Btv9lBB!@E8O +$d֖P|T@Ճ 4jEyԨL]>8ܾ`=PQ>UqI!+)-T6ycC |y{jxqx)t!B|Bk: 4v`t$$}@*8&@WAc2q =lEHF Á3{[nv3sz| SSSCcc#v4E7S!G(B:р=NŀA :ZQ;(b{t| Q=rmSLq1a„o_^?|y#;MU#y-W5ںbMy***X~}8H8f1n8K%B@ =DH3`4t=S.E]w䕝SO=uP(-//KqFymmmLoL5knԕ ^=jFL&KKKYr%v|rƏg-x!B`4S2㈮nD| vһQr:i4 H]m6^{5~?FiӦ1rH<ϧW\I{`ڵB{9ΝKFFsspOYY9S;vZE!??gݪ2n82337o#F6f̘e˨d̘1-[h" )))̜9Sgw9qnsyXQ w)Ppݬ^:"H0aQQQX]u 6kB!{l˚LV/ꫯ(i0OORL ̘1yq=i&,YOw+hn.\H(b…vmXVTUeϞ=eΜ9Go0زe <SNO?e̞={V\yznU|IY`f駟fڵ466Haa!s3NMDz|LCė ]J_TTۗVMرc|iBvEkb2Bc,95-}$4ķ#>> 6Ovvv g}ƬYPƘ1cOOO}լ[.b/~ .BFڵkygHKKcȣh=\ZNqq1} ?( '%%%|M'N$&|+*ⱱO'BLL fsASS ''L.R-ZiWƶ7`(ףk4%KŋO[ZZZZ[[=s5\ÕW^ ??|[ǥǰgk׶0<{]׶`2Eu)}ǀ [ jkkO~V!g&iP @BB ---[or(((zn(((d +ݻװ|rN'ӦMn;ƍ|~l:[ʕ+Yz7ZCe.7%%%,Xg|o7@ejl`H߄.ʊNMMts\(S !EGz?… 9s&ǣ>JTTI&P(b;DUU:ϟ]wE(bԨQU(Xjqv /{ٳ[x衇=z4 aÆqr9Cyy9> `tfΜ;dΜ9~V+ӧOI)YW760yxfYQ (#>'';vPWW@SS+VhF~~|B2c #- u]Ots꺞]ӴX]׭Yt]7jf4M4Mi_ӞO3voe+Wd* #Wum-߿Ӿe2/vy[lXL"S !!'OFUUTUq[WEWUUS%jPQ(^EQZEq)Ҡ(J-P(J(@-Р( h<_Q 4@GBm"f7xwKqQn >ꫯС\{$!"L C/N̅i fB]7[qHӆN6}T(EyO<&  X !B!º;Po[ںb>.c4] %ZM!PLVo{Rݒp/QB!@Aoܴ 9̶|Y6ɂ1xl6K8x=ęI!!*(oopү@rlkqZs V<lLD∵(Ig MץB?(bx-Dl x1D`P>=X|B!> nJ!!B!':&V !w A\Շm I !B=`3h0D\)س%kqA"6C22NA!By^/x^f3DGG@!'3CΜя 03XoV8 "IoEc5*{+.<{ݻs2o޼Ӻ]v裏u]_۔)S`ܸqL0AJ+."㎯%lz~ ]X$//KqFymmmL&yӄB3PW_o+:nC aٲeTVVٳz~aX$Pgͽ<صk?ϺU/,,dܸqdff2o>%/1-\: FsQYqwIuU%IItͤ mᣵq?wߊHi/$|?ÕsY<$j 2V\w$$&s57~˅L)WʏBt3H,}iY&{E>,"Hp:\plڴrذa_|q*OHHtt4r .$ pBn6V+g~2g#7 lٲxS駟~zfϞs=GCC+W<RVVFAAO>$,XYfOvZill{B?~ex=~Ww˨@4^ UO=Ʊ8l)}3Aºիb$Fx,y5~gHӇ}4{w30'&\?z#?4 佷<,z6ǟz^u }U~?o|;6Y r pW+ ͵o:㳟a̸qlXJw㏸cto?ߡ`ۙkXԴSƂsGԳsyٹz?aw8|c.?.Bq\.7X`/2b_BBIII8Έ%%%,I >Yz57pCի=z4qqq<31rq{.b0>|8*BIII G'N$&&_̦MNccc %%Aa6X,8ؾ};999df߱KټysDڱ};M ł39a455rqQT1睏WʏVssrvmX,ֈsEFf&9y .t9;`%Ż9C^?F*ÆQW[{ B07w(JK/̥_imiA&^|)!Ck@>ꨯc 1 7o>.Îs$$$bZࢋٱ^>\H.t}Ny۷nfn5!v_p!%Żrض52P #v_vO.GK.SQx'~MMM_h-"߼y3˗/ڻwŝuw Di.;wF>Պ:Gu8d'{(JIڙL&wu7~>OƉj#aEScId|> pp Hv^P~S^[?'$FIq&'S[[!);KߤxN'?%9%5:z[ʡ9yǜg /bF&SrQq c?N49>!T:Hz $9B21B4ԟ\sia蘒l^/{x.H ,d~xb|p얖pVb;p8koo ??|k/  0vWa4|98xzƎ;/m9xhiqLN<)+|<Jc޵MQC}=KOo)-XƝF>ʁd\s|n-^}Yʡ<ƈ+#3׾Kr}q'5t#F-I[6r$cN !n:Qfǎdggw9wt==|rN'ӦMnʱq ϖ-[8OWAA֭irpoСCٽ{w9 v~ z=X,EƟ[xS~}22j?bwۉl8G[6wa3ŝz8/]Y=.sٌkbX&%%g( Onl8܃'4Mcbw)O7xY>7>}K" YϷ:?GE!zta2}pt~~~~B1 EAtɪU ^x{=fϞ -C=ѣ4h=MMM 6s=}zt&'SSSXSUEqi}y,}}>@c :;G]fB nA%ŻxL__G<\7fZL@/jNI=U98?W]Ut:j]ݩ\{Ss8HHHdwJ!EƌÊ+۶m踱 sFczK :udɼKǩ,\07~&Oɓ#꫹꫻tm\r \Yo~~/?HWXX~o\:aZq&'w5֒ڿcGb?ċ/!eرcٸq#S]]}|`ذpK B.b]Z{•?*<+佷B̬FTUn>CuU%Pҽ%,y5iC`?ݷjn㴨G| \<;~'ކzR\z~Ny4M=P{ql6;{m 07x5~@Bb7rkt,HNM7Я_p!}P0H9gp^kWHjZOhvÚUU{\W^u`-|{#FȗG!zaƖ-[  ( >^ IDAT3fi`Bmعzmٿ.E:;eG6o v-(6 Aܔ0&E`t'ێih.\! Id)!ߪ1cKW_uoCк(BU#`={Ŗ fN[ V8;(kC7Ki !Boԥ]Υ].u +EQ2w@4Kꐊ$[t7ŇqMEBljGO`C )m!B!zKvVڰY+hlQAk)K?J`@fF=Ň(3rմL!xk(@bdP0 RB!B@CξMQZI BRK{vVcI%iRH'fM ^AR4t4]fbB! ܣc n3;:щ}Ȋ2hRB!BB!B!/"k ! BcDY-RB!%#8M;!{ǠDY-$$PBtc0B!7&qL& Η"B cT-]B!t]'  OHD*B!ܩ#4P(<%PB!{d (!B=d5dԫ$K B!]' !B!u jX[[{҈Cv;4-bߑ#*:B!B|ۇ ::rpc#‰B!= vK#!B!ޏi%g20#|`f!B!ܴ?NԺ_YL& B!B&lJъY }gIMM=nt=B!DSEYn薸h3C3z@1n(5c2hhh !!A!Bμ~쩖w}߿y e~{]IeٲDL8*[x&v{Ɖ=)lG$Jd_vnorK.{ @(|$w}`P >q}|S掠.hw٩A(!B =4ųG`MISZt]?!Bmh׾r=GE82wM0'/Jx{r3QF;ݬFv_iY<5·4)o`g_/ȿ_/\_.ޣ>X[_V^߸N:@_/} \_N, N, X|Ygm;mˤ9uuP PV@4t]wɿB!ăh'/Ob/\]͏>ͿO}7DvҘʼnV~/.*]- m!sOqҷyqrcDv@͑&CPhk__z a{x߱.n>{/v}>|X#m 8g2I>}'15nο—/𑧆 dMG_^᏾}G:xt0ѫ|KFzNw<jhysǎyx'?ǯ/+x|doa,;v_ld/_ś= Rh0?o]k6QR+B@={\o'Wxn3A^jݗg~I>,lK˜^'gZr8Nk}\F:48 X3mΙYHV/$dLDoW93DS-FδH6^ZhO3/ZBh~?>7 |R=E SvWo׹Osg=f`fm: L x2'wC˷oΑʚ\ 1TCm|2tӲE?Vz ,`5i\{-CKM8Y u5 0p:\/8x?X!m2>x1Cg6M'Y~#(TRN&WmZi&dr&L&kaF;ds&V",nj&|l~f~G)=,6/bCI<Ư\\X줳x]3ueǓYݥ%39~w_x5uGׅG鲠8 u9Bk+ _8/}hi B!,k:όukl=(Cm>_c ?ۺ4XW3~;͍-v29BI(lRF{77Fn_'kzI[O?$ Nqxzk^Z?/}IPpcq?0[ gW7w63!XJ[_. ꓟA~#0,rnѲ&˲Z6˲:l6˲Z-j2MѲ,i.˲ 4u454i|+_oB}UB=Kf܇gg8Q_{2p;OPЇ>T*jo),MLTNӴR*J*bJRjS)*VR:q`H+@0KjB!=y Z cg!.P444jKAr4=B!/<~]d&l%r`"ibYe;B6 B!H"/ir \4[s0i33;B!a`59B!BXu-(HB!ޮ|^A qhƲte. B!ANp'-Cl(PAA5rr$Xwh$e+!B{FyXM qh'=;zWWt?[\LҼ6O- fb;s!Ja:A|X VnϱJ;9:Gzg&&ܹ$T/ 4D$eK|JBu=!B%C.3xml 2n: u̝%&o/tjsx<ז$q?Ў2s퀗fK'@|6Eke_M!BCm/2]#q %r(6& k+:uGiicю!WqN&O6kn^vgPJ#)4B!jn9M@/~Itiaa@C#fuqmm0bsmVpP)u !B!{4I֜?A6+[BISGܭxw? Olt^b{;G_&ZѿU"ӆ4:B!. b?,l+5U(-%X)C#ICCUl{)@0;3w5e:hǫ ÝP 3"јrEY`PM0+w`2B!2.ڎ4MCӴI؃ )FJw6L&Z(^4ea&z(Å(G0MCLB!> ~F=47,nlCl/m<&錉>Q={qT)Lo#> B!B^kO' 3H`nWM,p9-kod '}w?E=ݦ!B! 0t,it SR#MTgNia{gz{-!B!2,(S.cgX §lϝf$$!BqzCvcl7#{` B!Ľ{({-~kckxDŽB!S}eEsB!_s՞uk4mAם!B!L}eZt] (=gԳ?I1 6 A!B:J)2,T9(@gr.^cU ̍PMB!Nga jݮ F"liZMP !B!+K^`کjH$"m!B!P,;+ `_gYB!bRz yza v矗B!uxkCQ'*):zz_.eϝ;ǥKB!ܹs;wΪ1(kFB4jP>UV=/_fB!"L/|:N 2+?hHTjãcii}c<<Y--- s+!Bwp8͛7yW^x;u Z߯o [ys^xo~pbQv{ui5M=G,pAB!z; Wګ͈*;[Fg U޼UA(\[B*lPU$UyLӬZapZ!V͜xPPBqPOPo>ᠲEd}j3V`V\v^`ߡBa_U;BLl@PJ AA!xBpP!6UڲZA ,{((.([Um{V́!AABf~#U U[UjmXP%4T f@CAYq*HEPKP *kjpBHPB!ă #,Pޜjh(+BA&5ZU+W *Ū.JX[PԨMlTOH|M  B!w2,T  UAdI- ee}BI rkrel[`UPiak*oKH/7+h!~[Pgxl{j 2{p?Z}l͊#Wdms` z,K,l2Tþ}x!e8ڪb8PJl WqM[Ѐ:AfG+eeX*&(ʛZ0efFҡYݾd !_p`ٖ` PUJmaVBYpG QJe,Jhz (#= fHk5Cx;!"j5==$T F!g i ʐPR=] $R)˲RNhI, aAٚ"f`lRR~P!;包f!ޯ>5V0Qvbp] z!,hMjFe5Ox;!Fϙ`ovT Ő ۥXO#WDlVl"T(M(F݀ pe@ֲbx,lAAUUef CBUrv/binpD*OfՆ7L=HӃ=x9ο6CƟ>EĖg] IL4MAzdw۱L:Ip<(GBV UܷnT,kvx%(5 *`SQPOVX 1 8 a[CQl~TZUYU?(HHs7c\6JVos2iVv V|Hh{ݞ3_`6`my#YBk[_1'lZФvs[md^'N-BfZR?H!0ĔRByWa~ {#Cn5QhN _KP RI˲JA:V@Y a"q@y96AoS_݁s&܎yQte_5bY#z4sS.oKZ(gQg+1_̀r7fbm իNƱ'pU~id4 {x>O?A.ZlIs2˯6: ߘ pSh7_jg5Ͽ*ʵb:*;WHOpύE)phd1"o\MrK%qӞdq=5 kvNa2Vvn3WcO=XF/&1t{ВaVC \p<1A/!x~a8a}ģ A!V ŠP¾KfߴmTP(w<:8K5 m'CBX< BLmNNhI7< /H= sWnm@z$(7=N3z<후웠esو FgOs&/_^ 4q3έ3DM'N3k`#Gغx_ySr/a"9uǺ_U`xVc'QVcfzF (JIOJ?£g8?guaV[[IM)ƚ/18L ٘ܨC-(בyτke)Jq@@E=$.yʚQOBX B<@݅@3m Y FHJF7w"D`lE4F3Zx}cUl'c|O{rGnccmP6LSkl?Σ=Zo#'/˙d5xu7y?C*&wqw,^$ W^a'ЊOmA4;BD`P3{f4 jʶ[ٙYnk +:9v2(Dk"|I!_6[˻ܮ|O&HӚ*-_sыҜ-3_$_XadJduNj}9!vO@vct{ σkܹ9B$Kxnh8bݦ>dN2v2gJMl_k{&)8>#,DY` 6ecvV4H ֈJ:'N6f8+LcN<)oyayJx P؜ql s=g}SR՞fuV).b@(^4ۥɑUZ kliƣb$F,[>gڒN(=Re+ ۇ1U(t~']>@#T,I6!iguD)7NкvLޠxjM͝qG_ok5:nQ__ ND\xܔ$n;6LAffc> =6:M='x{Hao|j-|` E4#?͐XY9́ˡ;ș GRqDk,-I`*ƩN o~u\ J){3tŵ=$;s=̼OX0윽Q>on ͎iN$wNs/MKLE,\8. K|WfdMXV,f|*a3 A)"+aRCWc>I6b#K ̭[raB?ݾ~7N1ʨY__'$g;K$E)?ne0 ;XI  8,srfÐ /#8Los?Hg􋓄;_FӢFV$;ć}x<y5"vxiϷx}1N*HPB8Ǫ(W\,CvqssmzTGX=,#l|`s0 B<(^y:u&ϯsK7Ь,730ʱ~otu25Kx1Ɲ 6A"z8 MGx1?ZMLG7<f:IK;Dp>lW^8 7~loׇ>pk$KW_bK̘A`,.J4y3I׍Fs{,tJY Nry=AOm27NviVž&B:WJTvVP=$VD;S|- 1V8N530I VF&8K8H'g#oaۄ [ :P\muMnϯ^c0^cġscid$L_7z8}Vg +[,YvFjwy K+m;6u2<8P| ![Tf"0ԺXv[Xļ:nF;&k*nX8vף AA=)SmO^hS>:ݎ:oZq::ycw צHGhrk&CZK\7щG)JwbY\.` !oa5NUvp6O880$@F5 &+83A+?I3}Lm͌v.KMPJxxɱwcۯZg;Qhw_Z CNu>#BY5t}`H80(8,ɴIaݰ`f N 򩡳9=>FTO\+#YWZJAC+={lw?9I.11 9~ս*ӛ8z Mv5lO,wn-QB!xz׮޴&|Tb ZSFuxEA #ymeTHbK!@F eT`!NF4'ݐu\BB", N :JHHȎZBB9]BX Ih%$>vd/i'KHHL#VB3٩K\$Vq aK2+!qۉ*gp=I$>Vq N$$vo3W>B/!qXY%$$δqKHv2vr&꒒%>$Vq t"OIf%d;9KdZ2D ąFbeȧcN8ےJvrډ>>S/[.;| ĎWǑu\BBv9:N9[-ɬN2\Q'gS>3ucLUq \szm9>%I1.sW_zg/;zOiZq 9gꜭVVYۇ/;t}\IH\h$ 5'yr $t,6/$vĚOAb&#;^f vH|\$ DUf KH|:|;gLtH2+q7fX3|_42Bls-V΍tuV$Yʪ9XS KH|:LD6fKWhe$dRa؟W,VNds)p*K') Ebat"fh';M'ڹ/d0If%> 5[;ڇطn(/SeJ':BRBBBBBBBBbPQ=gV;*; m:/ꁄD6.|:>UeQdH"+!!!!!!!!q6FfRe $6uY 3%9(VugTg%$$$$$$$$$$V*,84` FF &Bȉ업pAU@AAUcOQPP%vlwEσ5DX:#Kڄΰ 1>)WS9IFj/$\w$)'~d똔q5"\*4SW4UES@4EAUTŮjJ;yz=SrnJLfZE3H@H~D$X:+ii"Kl;-݆y1/v{l)["q<4Sx)G'Vڟ" B;Y6k]ݼYC`s|DHH:M  1h9̡&F$6B(Jxҷ$>Lwv|)_F{)W}~_9k;9.J7.dz}C " b 5 GU$Jʤ, eG!b0VG>tMqq"CoDQ,ˊd`BX1۟~,N9{:㸎3V yI;"aduE ;Nv"3(1>;8-gk]D B!a iľ`Xc2SnX^|,ˮH{1Js,q'Ȧ/y`d-%cf(0z^4MvlC%w6-?2r#L/wg޲=3dG&N62-lIkz8L 8Mgif͝UMnۉ522ͨO9vA4=u@ B89Hl' x}k+0Ȳ}ض}/a1VʵIdz;۱bdU8qNsMLWWI+2y)6GQrڧse'DQhÈ50 È>c 00 BieXiXeYXPb2eLd8"pKE: =A#|(}}4gO'3~'8rPNEՙLIrF2)0ktG%lt280 Mh]%9'먒J%8*P MQpt`pRR2SWIWҷ1%k ݱSReVR{Ķo;]ő59{LƱ)[=UEUPU MSQ5鷭?%{)1Nj7s oJH ";a56_"5e7}m7xx_vz4O:yVF{x9ȐJn^Zͼb{^^gh?u4z>)dGf YXKXA[w9Ѹ7k=ߺ 6d FnBs"凛gy:g:>p>YOSVJmC%ܧ餱O2zfI,v} A`"ND㾄J"@$LSRQGAh'z'0cXiPUJ-+>77^yؾ}udTe7az> %PP(7 E=:3n_Z:«<ʻ=qY7~şqlT`-:>RMjعJ ZUEH*q2+b E-_W.wibp,oq:Fi>8+s82@{V0%TsU7:9ay&x35sSC> \3λذ3eEW@ʌ[yQ$-|I9e#̹F\%QBg1G”L\-|$c y!XfS4%E[ qR+˧R_ءV"&hL5S|xb6GNb$hk* +ΪE*zᤞ Yy13/XhI<t1(w@ 6ٓH9Y} 2*hZX ~+EI( ?mY }M/ݸKgPqK@2Id(m17Ab(*j8;J  F vZl<[O>>чP85ȲZA:l[ \7?룥 [9_B{7Cx/?zcU͢6f^5>i2ϾˆRƂuem'[N[h̿1ڣ>.ҟ{;#PU\wxx G{?C!z[Τ03.7Y/bMʉzhEQfZfͷsͬb\@v6?YJZx*^34g^z# m2 o[@m` >;43܊L/m%徥:Q:y $!=($0{| qy6 Eu+q]8.b5>ƿZWA' c.U6EMi`n3~]Ǎ5MQs;^̴ L)qea"Tht"&Y*bX_=WLT_;7ޠ9PY</A]B,f[]ͼUeժeLvv焾͵BBMx9W,k2œ*bV.xAU_ %OoqIL =^N#v% OܫvA?BIs+}-߸j7`s`3Ur+{:*X2/=o؁kj@UU11VUU,5Z {wIPRm|ET̄PD(x|+O>a毿[UYl奦! o:p*{G^jO?/=TdG'# Xz+wohlwﵓnncw}=|s&Kds7Q;y'@ RwNvx7z,Xw "[v1P% M^[ݽ^̮V:ť)Iͻ/>x k֒21ZelXƏ_iK!>g٣ Wp†mN K#6q{|6bl<)z=AbsK9YVt/ʹ;,VqccWv 쎯 aэ8Zzo5K{#y+[RjY=ϒiP{硙.mM-͏}%.&Q6PT-Iw>3'~6}1^?X7O6M:Cش?[WOmf}FE. yA$y*`aY1eVM{#CMJFˍb0"<<0#0Q#BȲPsbN?!KP?!I)^yL^4  ɔfGX:KYV[[ޠ_•7\"뻌8)`՟='0c6P n6oZ¬߷h)%ohfKn$4cT,VZ]K_Oy>[͸zqT)\ Қw8*-鸶Vn**؁cH}Uoh_`9r4DeeSQwP2~"c^" "B `=4R]rJuMP#U*S4؅BBgXk]\p*%{{Tn?QEAl<&'yY:Z *2uGA˘2cX(G9[c.Zs 1w+;õ_; W!^+(o:Ԙ59a@``:0쭥j{ jaZ(KU-TUKʒ SecK'2Ŕr :8iJʤX4E8, i> Y#JLe4picjEZɼKm;Id*<0$*y2F]:%i"K4L.d.&K(4ekowa|r\o-1   ʹ˜u.uC-l3xtBP"hE)R}ŽrGqSBW4}9`Bi*=i'5_D͇z:F(BA6+jJTzȋ{b͝QOtLYNI`AY앬ᥭ<'ʛ 8wiQxg<=XʚjRl0h%S ~mrŕ A5 gȋjiY U$#&:EIcc7ljS(*`L-tGe$~=Dx{q8PȤN8-<h4~܊vS` w̍MtS{6^Qh.Hg3۷7pTCp]OBOoHt*%4knsi چ_(' x (ʈZUKq@wovg%y{3)-V|XWFP3|] 2gRE8b{}:b| (.gOt! Xzu5B ZHuLe`3m'hTO9K%sYu\Vo3N0LсN= 4|֪{4Q2bxD35x"XޡDO&cI^ E[ Q{KͬB<1NİRf:B0GWEH) ;*&ѰF! (E%#FC MC6bS1L ' |Y{6+!*PL@-P( nZ㍍4/h(9o-NEO'd58dWki/yÀky諵;,%v‡i[(!RPdUb*IXU] }Dܹ(&(O q3q/(GHx}Ym^G鹇?7}rG'Ȑc # )3sF`(U ־v~Q~fOѭH?z4Bg)'^x#2_Pesti儏9ΡHUòȚY܂Q #47$][uPr}JpӗeUcm/XJO6e Dcm/4vL +5xeqڨZ13O/n?pW*Wla Bh:?#x`Tݭ3ni* y" IDATEJ?!_9Y=_˱(BL.h WODa mW]*DG?{;c8ΦQf/h"Vd󆯺\>vu8-pF%~ 4w2n+萟~"]?fbkЈ!z\1u={c]m{]z)W}GwnE],+o ]c]ۇɑ{fw0q$&J^z m{}C`}Wq۽s8օI>@W\'PY GS#<^ݽ?4={ 淹fdocV}3IWB|Wgw UG4ǏS6vpNbj o-~=9@daE kؾ{*b/ēKOwqﴤ09'\q?w/vo$>rn%gwbW~)v&B'kѨh$^ >#^\}WOaV2?C* ϵ =kRC3/CgW7?9|fO_X43:=56oFUf?GL{Usp)v# NAZ(?DzIC%"؂TaOl[}O!8±m#\S !H\R5-a]E|V / "rx,Yq-G,=c͑#O60k3"v"hzނz_<8_̲,(4 QÈb>M0 _wcFl_01M{4-L2MLӌMJNz/KoF򐐘U7j`@fXK֏.K빧mO^1壒keS6'2)I̴Df O٥/wk™w lv~=#vV kOb^D7^0۝r?G^B/m1YR0:,j @|A[dJU|!).TRlNJ42HLٕ=h^q+_j^ʇ[1$r9_gYT%z3gy ]FU@UnjW6Eu. ӥ\1e2Uuձ\V +;6oNKLܞU5x e>q5dzt,%3)=`iu̝ *S5Dfg#V^gRBJK&2NU3=xyT\C=S9=s$ʳ^**+)-+FblcH=<O+NeuJk($׾3% ]ǩ*J EPQRQ @$ETas*f͝tv& f%Hn.›$frcW:=xQ);O4|Uo,vS^YɂE)$6" bg6U4|>U52ךzHW.]ǥkI#o(H4J'%ٳ ( SU\+p\3{+)J,$! +)ҕUG MC+r"HԎ NBi 0Mi >˴ϳ bo+{]Xv=bʿS+K/Vi7%Εp\ zʒAaDq'UҌߕʪ:|dU6'ձ<7!)f^' Bnc{sO??q,mr󠒣t0U v"T E~U뢚ݡ&2f6[J)o\|aE-./eJu%E>/_HBBS "I"g3op(R9~=RsMdY4l=K_dGd$聋pozfY` G`$$嬗z儐yqY |2JHHHHHHHHH|$$&fIt/>c};o1.;inG\@!u:};%>@qq1 ,DVBB0s̑! ;PVgX^.D;NDs 8F͋--->JH  $R pΦ4CI;oL8?ٯ9v {ƄT5/b;e!% %mvDx=ɺ'#+!!!qHnwǤnqS|Ll|+b2Lp!5\ZcGVde)JHHH+ Ƽl=x犳DJϘsp$P-N{4&2JNGci#+qq|pď\/%!!!CrE,(3JF71 d<%-9)v"2yDzqIK?: Rx)o#!CrP3/[vzder/ie*dJH"+!!!i'b|53Ϩ\R>I s)srLY 2Qvm- f/kM>q_5<~Mksݟn` oxl O^WW' O9rɷb q)̒| |Yb+˾Ӵ4c0PTTwU/a^}ZڛCt;3rUcB-q 1by))<2?m%b˯*,guD|i* EKd(z]"P\v1gsZ0|IIf<61\CfxgQs},+>Oa|4RG[)ħڼ#SoίSC=Ӽ3\3\۠梨n)Gxp33V'{^k&J>مټ̻s}B LYÊz=g>KK}]+Xl^ GؿgJW3|.1t7/( KQP"A"xI&OEǹęi$V]j1!@ {)12&{A5~.ԮhmP2Ο'wg>66m8 ^ŽCDb1 4m:uFwNŎYwx+7Qځ`/CFpn`^= )i흝饧oQ} 60Mt[Y]U,}#wOW;:N+%P> 5Dkϰ׵| sܶ<6< MT>[G #/odDz鯤"˩.޾2V\UPׇFo<;@/U_{b ]ŊcVEMy=;9'dČfSi>2mJ:c'le4Y=f Ǯ7ࢴH({GMj~:ZDj\; E``#2 QB1_JR4 \VD¦Yk}4<^/nM2iE"hn7n" L+i׸j :F`&rRLƍ);f㴐q!f^~7f86?[NnYT9f޵6z BX^ xm.6~ѷ_f mţ\ww Զ@{=DC +{ygXvI%%a+OoH~|i5%?аTd%$$(bz5˩ 6 ^&if*^II@OE2v"0o˪u@g׽D_b>x|^4]GOʆ:VrI0Įc,{p*Nn=Pz1BZ3h( h ,nꀶWvYVLYaYu]~,)2-iҗs/}oT]'ć*Y@,WqFУ{xe4ROQx?@w{ki,˼~2B?+0jx2Ȭ~n$T oq] r(^p3fhj3͌?\Iy!JN"ܲCXfL.FwcW'uWn+s͗7Ɣ髸kctEG=A*fUlTVI,=f/uT&^Fe7RwٍGf?n>έ?7beNoD#DǮh$`"" J4b֮Z3Dg&%.+b *EIz TR)˦R[!b/q1Y>|co?>/1 \7zzm?ڤBӈG5 P0#aJK qSA&y]ESX]4oh?EqG1ڤ ՗LMYT;/=#P9S R#X RV@+zrȞ=;P՗5my((5VшO⹽nPƒt]j 2t(&JjD G LOǩ^[TP|c^ 4:裄B&^R\h1`F!м*`` ۅna)6I*XQ,,D$It*Z:⚒\3'ы #K^{Ƨ GA(1_npJ6vå^/じx'xU02L0C}\Y f?fvMfYl`ifZ<3ZtR]ӷ ϔ+(M {询~J^ voP!f]X~z lm+糯>䄳N|UTx/nU2 cJ' ^:S'`꘮y}hZeS<>f۾6Aw5l0"GO"(L49/u x.4\bjnxbV4"-opݴ"Wx丒[Np@_m3uj?&;O4u7pߝK:MaE%s?'^ )°~QŬbG> fX0]m#R疅̬mT0!KR+R&iI5R hy)9Fw\ IDATbf(Phq=. $"ܔLBžs%^ KhTOҧhclD0UV^Qp{݌F Me$E+*إ!TMD5Z#V Eq곒 ^+!/.Z 5I\T!8=7lkf^1?IYc3+̰ftvi.4tbY9(1yk3o8e+` M)2Rdʽ]_Y= 4,J]v u~(W˨,7>ى z>kt ߇c;?Uk&Qw#}p9]̒i}3x}tk>/^MGS˨/Sxn?PFM:eUb̸rݹtzy%'hھIiADI h~緟KaaNm8 "]?B~r۷ITz3pSK+(pUq8I@5 *ħ:3MIL+=xT7n"<:D;#"` 2ÈDM,͕wn"#ea*0PQո DbVLŲ;,3J԰#f@`Yf` ͬيf[+S21(,6s,fl崙Ѽmfɸ6(6=WW哨֡bdmG6k#܋ Kл8BϾ{ #0ZEd/`ZCeTC~" 4[9|uRXY1Rs͆x?Ø櫠nꩠKah[oWrI<J+gx\J_=WJMbސYк S>˓^1BOEŬ«~nݍlzQvdūoRs\X\6үۚlW~v'hf%7o?K7 o):+վ.j(mFZP~?VBJ *o*ҧ^?M)vίfϯfۼhb&n@P`ᛄIϼ^\N B ]WM59> )TGQVVT=S\E>E&r!pkū j<.4„C4Օpe P~%¥E0!LEEEwyDSPT ղ!(*dT䱉(#VḒ @/nz6m x_KBLr̫hf3SlW9P53] O\f!zT4f=퇊Xt#unQZVOExş3Cd|JV|~{K_wqQOf_8`1*^jҭMUZSzjj{vV i?KWQ!%: ?ߩc9s9??"ove Y|/^sg-Q߿.ϮVWR@xޞ-BF/Tĝi,Q/~Uy= n:^Qw뉺W.6}_D|i Qz<ɋuK)v͉X&7p9YدND+&і١uYk:뷘@'Ottt/=xh|.V8'5:%ٸ%YSҚRJkK`ӈB < p4N14gO0$)- p~dxɾzKu3;Csg86 4K|v)jKS֡ [1#n6qdJCW5V`j \G  5(Qow*/O E5{eERw+=Np)^攽qyd~$BPi`cF|HL&/}hOV5TYIuc5}̴iCCA/Txݍq+{gelowr!}U]aNu!+pG YCMś!bҰIv~_{$#l*ބB6L^Z~Ěyd[L{%`lLĆ\[9{ٲ=&6"e(cIG{%ϳ çri&y2~r|m2d쐣 d3Ӛ$Dl:3b@x$t~DZ4W4!s\JkJj"7u}e 5 ҙU1@0ce6 CłӉN/Jط܌jwO e>gjDm3Nޝ * G9jψ}K:k-?u4[^ Ψ!~lXFУS{/r$=NX=~B. Jq]^m!+p+ydM-&nÅDlG3px#5@2_(k)obk(QAΟBs:XTbmXZ:/)jcl+C`fg̩ 1%]Ϻ\y<*'Muqlz^m]㸉u_qdՍՀ;zǑܳݶzm_ˊi3 ][QN5 .6;CO݅H*G%G*ra4rlp]2^اgw9sڟ&)1& ûyS{Kj9'#EE_qnWwb+ȯԿ^9e.Ѵsbo!7Dl+?jr-?e(^6ā9D|`#!rXLՃZǔdh59T@p3ǢS'wY_!#+{lR (Mt||Bm,{"Kfvs#Ҍ"~@1e#[q~pLD%eP;M@is_Fr|"0<2*X4O) NL!!JZ+@Ur*OgkIJRHd1?u9,Go!ةn+Y{ir'I$֟# #6d"d}/jk<ײcztUU~8CVR@=BpR䮓?#H ޝO7PAk<-F7- !p :V"RYՊb>Yi<{}="{bg煺{`=wcՍ\p:q N[ZOqRex;ۿ*PƨEk lĠ?!E2Gwuz8]^n'|{X=up 15(,3ɑɈQTY{-`>d s&..q&M{ɼi":iɟWsNǬ^eΈ<P͊噔)㙽p66e y>GO=])i,%Sl02$Gils:}~3XΛ).rO\iKHpobX) a:zdNj K0!bpU9-INAM$"[7)#bݢP;%{ev/i?P9&"/g( *Ou4<{oGk\LQcQ F0\Zry]IъHܞY-drY'?['V}:|~pm{`8GD2RG9H9sYj09.:8щ0{^Rו>]5zx:⺼zhnˏZO>JBTヷ7( e;SȶJ_0{Ir..Q?I!NK9=t:d>)ơ;oP$`l uMcy̘vQlQ<8})/˿V$4H  D4#<9G{\< O$;Ty5q1/i:28@ %2]<9ہ"Je[<2 o\%:&4Qdk2Qc#1留$|秬Zy3SYn07aŇ~]_#wb5ڭ"c0u!zFzȑ3blr~Fܿ&.i 'pyqĠ"RYwj0("D'BQwߕJ*`h,1 sJrp:IDb]H"A,&$EIA4ZUwXN$ry7 /{q5s +vVj^ ތY@mz?Ы8>hZӠt6kNl19W~3/gRb3Qoj(tGGl>IO|5(%`njjXXj7P\J C3Dfql(AN`(B,v8heTT`^(Pd2ʡ_Or׫XӄL&lmΕkbr۱,]iKEIBvgy.]lQ5['Ct"S!xQyEe#b)d'~% -` :q;c[gHr9;i@l}/Bt%k>?G܋- ]@ }G}8!ߩ $( (∐C;L46`%qxWPPImU=QSN#Ѻ6ql=z4C%>R[-Xl^s*jE1=gP9Zk"Hu]`*Awr>@L]HL`0MG&fq b)bDvN Dؚ@"WD ػ$u v%R:`XΞ# o 1#U4w4H͇9]/? 6pOh& IEgޣr/s.˭zK/=zǮֲN7HB%Z}E,FfS=[z3RGDg!FܼE$%#rZm`)&XX1P|O, blMI?wS+ג2.(B5T8R@ڢTSQJ%Cyjɭ-#زGbJYBӬ-x3*%9{-X@_RPPxDvoxnl,`j%$Ojo$x V cLP?~woKչ1r0RAg3pPZA\8+xm:yq_}=dpcd4zb@15\7JI3 tdF C3sJI\p:]hBӉK,AB@˅)Crkď}9}R|ү.{GG";t\] 7pFJj^v}ϣcnE Y ;=qg#z>r ||\. ~{ۘZL̊u 猬)\CoxwE Mɔgp(J,(-M&j naUyKNpVe$#XfKwd}0L{)I݄x-XlVV;$(ݓ!vMv@>ꚋD@3\qNӐtir:jo5s*d|d>w|wOP6p Cz/]5c40|-+r q<]+_~fbW~Jl\9ee-ʨC$p] bdr9NAKK R/^# p-/#+p aB6< 9#U U׭ֲZph"INH9j_z5w B5ny$Ї!k.Ȋ Md8}%{qX.`۰D2Z."d޳eO ! .dVJkKj4\U Um)Å+w%H}L(K#$݁դ;>(.DRq-<xk `Sk./8(4G! dn%7tikӍ=ÈET7V-2$dr9^2wRYQ9OXXΒ#Y;?w# xk9 #ɽ*͉oiOo o+ľ~7^8!k%Oϰt]e @E4\k>9 J]DV-P["d ]geU5Tr6;-r%flRoD\&jw@#I1DxzևQHl*DUc+$< ``}m3vU7Ve:hإ*+ ЅИdt -8ŃH;"\E߲1Z蟞O@D+XV ݔ9̝C!omVO0wn"urj%KYh ,xyzh(fCN;_>G0j|PYt SM=sXldmذdy'k27s edg} 2%ڇS[=$𵌔?'utW?Py9Z54Kd$9ix~ɥ6Q!Qх~ʟ 43m[UZ1J&fNb{VT?8aMT~$LBUF)s3}@ IDATNlEg>N2^yW)YEƑ(^KPK>i/ax:֓QwfȆEٰ;E35lZ(57&(ϲ#Qn220F2JOg+MټF׍-)Gt !6][󒞒VLgX4b25u (;eDt($6J~7_yP*:w3 j5$G& +-Buc5AAx˼ '<0q+i9jCpz;+L{!K'Sug076"u[3D[=J7)anAm9[Rc:OuGk$ɣ4mSJP46WslMdAEK B֞sot|ٍuC4>vSdhHQ uMv=.`ME',g66jqvg}Ɂz8/| S' Fϔm.N^p!1:)@[-c) ɲi7'BͼN"(Ǹ5<$6WF@B<9oG7Y{Qehi}03AOec(K@~=_n,Ǔ"DDJy)֭G$/<:Ɠ=6Jm do:o cq7|_xUJ(n MA<5% JXuPd2$~8ص!R:v0Y "Ԅ q IJd߶#!wZgw-Z=s"%uRd:?50ˢ0l,<.VBz*ns5KU$" 85Fxvh/O)4ުfe'MDjdoA5v3fSD/Q`wJNJfr UnFyRn2OMҺ? d}K6Ϲ8opDǓZtZy2ԁ(DVkjW0@#TQ=:04F}Itk B%9Y~ !\F:~A}~> 'p~b1Ku)4<0@߰1/p@="Pت1 l.dbHW6 ܈q c9z)c*Ki._ ;|.>\J9<ϟEj9}MшTL|7Hf2wGpKbT(,W\NXkr(ր7^N%!Djx|\f "OF*+|:1ǛiڏUb z osؐ62@x):7k9)nݎjģ<7Oz͈k;ݸ_*tIͳ3&rV==ÈRXU>1GV̄gg3Vvt} m9%|GGMп;팛9045_T<<i@.Ez&/&|WNé ;'ł]徐8ap  ?]C YH&=O_*lh;mF-o?}lfD~sƪf2VsγU~]-lai0ƆwȊbН9@NY{#"$B8oS48^Vʾ8LD G $N: ^! a` +l*$,gP G)@:7[NVP&bv&VpҟfOQR@ԃgr2gʋi;Dn~1QA#j=[I@i(-=J2 ܉S f (/.'/` rl6cm65ӤPBR@o .cRUVJCdWް@k`hЁnO؟`uu/ND"{@bZȩ#a횵}YhlwcĄ$h)'X@ 7*y־oQ(dX,?#Ä<t[ 4т½FFTy(aZyj*iZȤ b{5M޺5kXYQQ)>̽T\hbu7UuvzGj\:Gd((բԱޯZ\/o^V "qF$ r9qzkgE^bi`p)/;BE~žjNR1NXP#^a?KPۛ$u3r} r po V;א?>5lgF4!h#Go#{Euu=ҏ#gUx9VZSMXl]K7cT@sM W5@o?NBV-VkX(w3 Xɯ֡Bro.e-@5.eь܌:8%`!s-8`3ۺ:M-ek"2JwǸFFA,-#Jqa1 "a S*іԩ:L7&c ka&ْ10H^F\N;OH>i;"D StEU)KG \!k)<^mkґ2__|U*3V c%ϛl(| W`kTߋs8 /{tڻv➺P*TݽdϞEL{"Q9OuW=x]#픟R)>̡g1wSݖK ر8U'<0E!mB$#g )ư1|{ɏh4RŚ6?Q ;ue1P VmAtz TQZlB?F @X-(w(x[STQakT4^z>f a~&K`up^U,[0z?[_oPl-4Ø<5>i GA:2IfƈZ_LէOUx>ˣ- UG1~Cy|l0Bĝ\łݥB*rlq Fȅ~h(E/z*T'SWdJH/e??\i[Ȋ|="d hПHtF_%{&a4epDy)e#ɩ%)"ySd~qI3njG`#U}0䣂Mm4&=cڜt;"7Ed{D6cctcx`NE6? 4D'sfv@vcPźsGlar/a)>8GըqawR g.E*;jb 4RUvע;F+iH&? LͫuXK6R0 SEs4Շ X V_&3#Y8es/I,UPYc:s\\}PzTMbDRRO$/ST= pro9-J)9%t" %5\Ы?Քp\*;8P/'8Paakv,yUDZ}GDrCg-OLdKiGx?;r?^ so/5S꾝?o%;evQ٬’Gqw8qw:~]H7R-TY_MhPY먷ة?@Ֆ1A b;vƦܿ:#Izj3I.cxՕs>uV|yO>`%Oϰt 1W+u[͋H]inE|~}:^1aBDmU iDӑ7g ADp*sסtX5{ do1Fҟ(l^o'Ri)I&׽X%*tS0w%& bcX%r̝@aO8Т$ FP%j"b,eھKUw3!eY:Z@2j7nGbbǑ*nƅ 9#[؊>DϤax 3 (Ƒ2/\UL·X&g"5|f*yG hHDժT%:b #!anqioJM< ÆM@Lهb4;=vo6cVvJ4vfsQ-y{l>ͽI ?'G'w(J|7Qzd\ 2D^?6ճ?ڟf SM|^Oyޡ; &cv$x_N zQ:()Q[O[DŽQ*81~_?ky2j=TĎ$ dC5T{vx@8~hj jsF(-PiP!EN%pcb_1桱<QIҴv8F$|'7Iv57 C c6@[BScSѶ@+cГo.C†UPvO@,f >J%MR.'Ԍ{Qkh)(#FYZͪN!)DY,m\ J_ڧQ, 7f .gmtS1[m 05|6dAӾGCscdȻ1W5oq !&/\cyf23Au ܔC E-1o]A.JG\Zk^]FAݞ\O/+_g hSK ܟC~eYBD)#2jM~9u_g<[ dAVj$z.SkJO>M@a&yg=Mfʧjfw]wz9#O"> IDATM=S"4wL#GeQ>5E3% %˗|rLd 6=k(n{d 6oDI&?w@Ń H.GrT TVu@I”X摵e4Ym$Fݫ z^Dp'/ }忽RD[|-eT4Z+ȌIncYTR dO&e\(e¢uRs4H.5T5VLx`x%ϳ çrig~@>z>w1!Os8Xy&uHqObFL`:Ľy7Pg#22I@hO|n&4h%:b g.4g1 ݣs!GI*sN"7X@p_tp&_^#5Wp}*U1 $H1?aJ*/E`!DE蠡C6Fk/QVۄ<9Lœc >8J)$Ncݲ,~9:G+ǧ2zqp N.Kӯ/*q:)k?tx8^~j0"/n>!z+0i<'P2)lּ%ͨCPJ1m\ 6"hbybh$Js9|F\"E\Fnǚ:̧G4;!{mZLl-u]HxZl-|s6o$/G3[z^WB֊q[ÔP].~I,C9PCqQHًe/cW6F<:a2LYВ|$lx-bKN!ALya[\LMŻX,isZVK :IE?D2gg]=@bg.!vE?ۍ=Ģ& ?nRH.޿|h J"ItR:,J2Tg%IrmK6SOq}g?Ipk~,}/prFouy[8gk`@ *޴9]T+hE'X[ť:g1ǘFڼv 0m鋙;32RLhQ+iO*nnhbXO{6l2kKjp0?K{/s{|񮆰fk’ÆU̍7BJ2N緝]eaFfgJ"+AޭpŤ>)G)P8)#'~iv_'?zJ}s=%GJH3 yvs1.&3 1)YpY<Ԅٺů:I}2Yb6o!Gkpy4`-`h~ELe;5M#4/wz Y|I>p:gq3Í{?V\VfYi3 J3KxbK)&Wt F*>ę`Du9j" p2XٺaՍxLqo r`RCأ'mYe1. çǑ4[aA$SƶX)پn@>WoA|th:;:'tP4)JXDBk#](g LdcEv2fJv@٤w1:j@]15pu3J'{em)ď&VTFEbd2Iώ6bWdAhCckr"E"_$ %Zjԓ?Mr:&LBe%ΖV0#c4}j'fǠ80biO=pO2#4GG9VJq9$1+域DR|7/Yc"%A MT;rSf!UX8T"l pәܮR9/?oBTǢ?>hdh1 8(=#; "TKJG O\T^!-hId$J 9zsI#+=Uͼ V 1)' R\\b ٿ )V' 4UIo׎e?6!!4:f'DDEos er*I3C8(c=ӟb>|]/A;.}z8=i&ޕʛg0o$FH45"Bjta~9pPHJPg#WmbY* $ ZKL$J8;;|""o0vT[ &8YW5D e8L%MNYN% ׫)6N0g;j5@6[GHP5 bt|Q>Z:bPB[h )f;85EJn2 S1v OnDpr8h0&!1t#`It>[JlM-8 zK![K^^ydu+˦%ӱ]He2$g=^HkT  v/xu`.ۢ*D..f6#&~߷nRQ5h&W`.b?;;&nlh8}bd'lXcMXɃ` MP= G;nYYji#FO1 *OdZ$hZŴ@Eث\yn&1nn>Ď/\CINt8k)ZJA+ĭ5atjs!ˑ2oڜRԊ]8N9pkMݚR^ S̊60\ K+Ř 9S|wL7Dk;0ۈ  qFJt |CfJ_DȏcLRC"^NjO$F*htbD_E"5cUB1b0BD@@G%dmm6| ]0uuf7_mH"h%[QJFFR=k[+;p(f޺]v] z9g^F -RBM#GF2a,&&a!;1S8j_YHc]aqЈ X,F,ۋ[$FMdR$'.u^sbo! Rv \tA9AQ F , Nt.vp1gnju4Jqcs?nNjhHD/F1j_\JNJ((uaZTI_N ?(5d(;271 `d2 "Y$r5g.I/'G*';vč5 +]2F=g-0L2}ho S {U)_|DuM2:%QH=10IF )G!iTQޜiHǴ".j )VI5x:o}berJ]0%I{k$J{AQH1;ޙN]9I#)^n q5.IF6,I$&]S,W6"QIKJ%r >i;iA>H,:6auqtLeI R.qܗ!X3.9 #ib`bvY?Wx]C u޴8 0N1c> LͲc˷;DaAz$ y0_t;E<]v\fL2]5-p;o(Z3Ι<'~7Sf{OzuC:sG',#w8p @l>CƍB㣿i.&@[ӎqwtБ4&U~}T{<]wfGMm"Ew}i˓Ik 7)<$GyXp#3ٻ/5aZ;V[|R4Exe(pŻlվϱww!{s!+ qɄ:JݙfGXccȗGI]he% 6=4;.g)&F%]q ܟu,~`<*!3*x4b:90Q-$eyJb ].4|RŽ"̧ud(F!͐FҨ$BF}s= %6, XN֕oD4_")3&͓*|S1yh/v}꥗7:ko֍T!5m<RJɫ;^)3HQ' oDH0;0RAx|Vh(n* 6<6[7PB(J ytO&UÍ&KY >j(Z~yї;,$+A v+[ٱBM3ꈟ: hO 6` oʜ1fB܋(à_kUv^;SfNZZr˜dE9T4z|h^aR>ɮe'1\즡|oK;.q$ w`*E"_iǺJ840! K-h'/}yu)j0q3gN;FA^ Q%0UcOAh ˑA.N:å>I{ո cⴻ0 J6MY̟}7OŲWQTx)E`ќapj/s`[,oQEԀ]?T N%s0=f&_RH[d7q3d1weXOT3sqk 6 a}3F3_&Oƈ*pq՝ۍT?Phk uԨq{|VQD'pKM(3-r7b_ܯ8_lב< 8coOEG|w?HZ=Ћ%e5l)ô8{_"ٶ{#E4੡評RↀP)/? O\[#mn^/1T@kd Z^ǿ FhkZvq(cb+[OЎ[̠zF*jDʜ4Q3ƨgp '}<>$nhgS?OxO)O}70B7L;[(!8ܨq!=k>0~}(ĴB3wɟoa[a]|o/nDQDw2h/t'OOX6"?^]4ZlĭbQ7%QI#0v'v?ǖVOӁJ5NgNFNƁ>``|Үo~ V]*#w0:ƫ 'gQ FϚD58j* $BC|ik@5z_IퟙD`BQnADw{:(=f6,؁69-CѴ'13*pdm8%*wd9C6(Nul  ml c[ e @vb.X[DP3vV'Qm)+璷m]#)^"ƠQs T& {ЗLAs+1{U!9s0L?ļq^[GƒL%Fl 6;oUP[J%,# )z]v4bz/RxĒ.zX\ -US(~qGץzb ,"Dz< tP FgZ kk]b"f'^HD Sz:`"F|{f~y5s)f+:FѨEuw2}kH cɢ1ghN(C&\LvG= C HW-q(5۝N\{-]2"o/n C*Qb6'(BBS˒ʘwPi$'D}/ǠpPUWhpfbUvrr jlSQ,^zP'@ u.N tc*pPt:7 ^ьc|~iq% {z!: eHa9]g,P IDATz.e9le~ py6:3f@eGg;D9y֬7Q$Y]Aatrrdߣts>5V;K˜5.e 8(yހcaӺ"f;Q?ˮцreՄQCٸ'iΑ] #mv%_A4$ݏP^K^l2_("1~Z14U4!+xF7d7dtN'<ՅogWbYGށJI]o`h;|xH 7v~oܽAЌ`|Ԩ|9H$XF'dbġ-Q[q Gp*# nəm'i)׌$!-!r(gn}u)d=mkXHG#|P~g8+Ia@vO̡&223SUmQ t9g*(Hq' 'l\ψcfCER[~[C ,c8nؾ'5X◒=KMgz'% }ҧtXck@5)iA*椊>FZoO`slcc`H 憡ŮB۩Ø@n+e< i@f?# dw j|^M<N-;?"g7ۗtA̼̮5[>Dm-{ XxsBs1=O^+˖SIJy̟!ȞEImahُPYش<&c\V=ܶ'l䳺reTi%<#j6Tű/%> KW-dײlWrW%iz59 brIG@<4Yrҋ_B6Dt5-ۗ4k3du5-DrWS}BCh3Z"Y=E #b%M5C n=SQs;D>^w5_8gu) †f d?=GCQ*-n ^ "FGnRjwKb#$nBŅ61{ـ7:9{LL濤S*>X[Uh~K$]}Pp+e֑2#۝j{ycouqlrw4ׅkV{0M| jMڸvnnMV!l]"P0R~D_7S6~FNj_SuV]] ${].V`jjFVw Rb?mÉi45ktkC,@OTh% "H&n! B,HN7td+^ls"r60zn;GI FH`|v%dOGj{M-gl8Z0@ 0$;ځv A# 4gheJ)x<>Ii-FR5 nzwG HhZ&fh9Ky>.)cz,TV]gX>= DqbD($0E*KH^%I*E7͋P-ZH {Ԇjhر{]2lXTWE{L"ʍ[ȪLJQ^%QLx/E=$oD&D9)5k ŵsh Q&OKy)ixfNS]q( 'UrB8LS'1CooύH|.A:> JF+(saHMcj4UwSݢõ}>S/o7-4v+'>Qfb Px3Wtj(uL0}=-I }_bR o H}Qxny@|X=:}$QFH@SV$j-)ee7u(!S" aon,_`ShQ1bf6:˩Q)\3}£qA bྒ22}~Bg>G2|/)9 w ArUm7GNnҞU9>ϏE?y ю"u<e^/P${%`^N5E+֮=gZ o l$Wjp3LVZjT@Sn^ӷC@ğ/9D~Cirp1d4,rXƧ9B$t'󛇕Y)ɰ搳h";3x(Nd|,9&/ޒM|L|,X{qɐ虵x9d 0򇥥h舏;ʎvQr1qdgө117ݖV w0]jH f6e;ou$#zhvcB'$Q>zu'+&nnbH8kՎG|v4x.ד| SOYvUc#SZՊ=_಻)Sq-S=v `W]zjJ4rًP^w(S,tϽKt$erw7Q//Н1cG `ϷWF`W]b*gċQX^9!/% 6 `7Nivc.n,lhqMڟ2͞e"r6f-pB%r;Jweu* X[LBCXt g`se,)ouo}#w0QIɺǀS|&-8{{ɹsiqb i9kAL R9);ұϳ,I 2罉$+75df-49eh])&-ǃ|﬏ d!EL6^hDY[V sR؛OrOTM%Bg8^gl}:)!+YD$|XjOP{Qߛm؋|vRlˎzj(?0[ IdD]݂/0Rg_H[!_ \BGV@@@@@@@@_ BV@@@@@@"p\BDHp:dr.k%f  88V $DƏV!+ CE\`С:uJ?N:Ehhe9е@@@@@@@2db1Bd\?~Ti[Y @xe(p5}嶳Cziw9v>w1q_bZh4# S] 8NZ[[9uRkhY~]BB$ &|>_~ b;[t^޾;N!g}+ijjzȻ H$^/g ~٫Z  $oG$ rz]^^6G$|խ |_t,?\YA |BC'Y+ O71MVWpnDO?H[A0YՃ1s^f-٨j`A\m7G2Xbb S t!~d*LiwЅ"g~ *dmm6vٍ:<c[G*Z gZj>q0cSytA#|39դjd},}$)jx4fH$L]Iw <6Nnlmɪ?v)O;b^b!$ΰRܥS6=ʦW׮XAȏW.b{bp]6!yl؆@11DD#|`8XOY;SGUc|93 3.:!~vc]Gz 8cޢ#w@b$cwF}2'6My,϶jy2a>}B}A([v$Vֵ2-Ct,4ϑ lb??&TI*Rvego9ӑk<;t/,B\kՊW#|`8i4"B";`:QXQH}s=[ʷ00cQ6?X!z [(ڰ][@Dw{:(=f6,؁69-C yOT(wK㔨ߑA =Jl_z=_`Иd.H&J**)m-26eS,lXm2o˘cm D" ƙ1R5ۖq.y ֵ=U^TD&\SY,ht[*#f6qc%O"SʳJEeu>Y l_#LCXN6pZtbD]CU<_Q0ͬ~d/ e"ybg78$lJل,מJ֏N󵣌tr'Q3+h]IL *eWy 5'U$1Ң} ᑺ, L,VȎ 4+qu~Y1{Uޖ&1{v~EȚkl؆IHԯED[~MɑJjū(tfX^y5+oaY$^e_R.[hqpI431i#.lo?N<.O,9RBUCuuD 6,SA6; #h3_`ٷ-{^|ϒKIq[~9~gQ=ocmlA cMw0oYt.K7.XH%)!Ctʐ0yռ9]D4 PKz `q?FvMΌc@/{y9V}KBn#7"D9y75d1 9 HO=J_L͸2| x8+p~M느ߛMD "V>,bWw!2j¨lܓuHǤl\8>v%gDʩ8c@'j iX6HXl_GYC"{-yI{Yik0 +ef p͇27~)@(WgyRl9{M,K؉Z+߯lW~]-;_ 5g]~]>)%M$J x,t-HٵC8oyӑ=;=}ٳ?Ms_R!kkj'";Ihsﳭb!3멥O9̟a"։uSdI_Gm XlcB Qsx߅N$ Cm| b&<KWC ~SH3rb~픿Gif~/t15%!"Q8q3厱M?HRQY#DaAC QTcj@?-; &[ q!j=nlW?S/1z9c86S ;VF'>U6fi;n&M'.xtuOޑFzjъF m!;v󧐰|# /LǪz4MZl\Y dZ{+&f8^C%%GJp ObzJfH-GᲥ(}'OG+ MCoPZ݊:6+lxp5eŤuݹ)2<:_?HξHݹ<->-IJJNb/ybK){jXϳlgy& <ϊ4X7gMK_|{C'(t07W9h?TgI[3X:9JTIP_?G-9nHil}Cj4khn5F(9RrnZÆq U9 ͉u3do<[~v=5_~ןY{ˎu<ɴ&jRyJƈ,H+KY/;B#s>-3W"mx?ϘJDSEJ8owvE+"R IDAT ArGC'%s e`4 w}Nmb"{]/n`uԴaK:v!g 8JTʮ*1E5Rf)S٣ްPwAJűmI)r :V{0hPw\"Q8^m+hۿU]Ms9}mG\Y};-f]9)]רjsHDQ ̚iSCeYT9.Z!&N+/ѽ5_MոID+ʻԲ@Id|>:6 nB A(虽|Jzӳ/s[`,}ٳk@/r~ o#rlm~"Eܢ"֥G[_Ɋchxgmddx7b2wB֬d/c omEYV"M՛ۨRů"+3Å^{(|VWw:BI B80lqW=393ߙslWg=xg0"# D Bf ȭӷ:twR]\)]Uj}[[n4ј7cܘ[wc>=[srz JܘK;>̷Wֱi,!o8YD ;8&}eߛ/˖2'ʗWQ*^x څڭoQ_p?0/MVc u&D)rr4)/?݇TSG>^O*l$\HLIgϗ<,_eZDžN@{Nvw4J"b3$&imQ#o8Ngycɷ9u|&%pӍM4u؉O a#tQmo$M;Y"c J/\2 ~CjmřHRz7쥣}_y7?"_ %Lw)7˸y!x\ғlv_=ocU QRDT]ų1 %)ѭUå7x󃌏{ա?ހ(ȲaWEQzreb@\ #3H E\[ά9< ˃LȜJ4~e1kbx8rn$lb:&|,>)9tFv-މ64Լ}C|;QxvϓȤYe,Ά09;g3LkI}5GM4yڃN &GG.8r D|FH+AVZ(m8EZd ,`D<&9Qn\ӧRw0aBfLI8?D>e!3J&S~c!N#pS~.=VAc~9Kvi#6 M[[^pkګ{w9-0 vaǦ*GgX;+?矤b_p= gv;uN,15qP8+͊_>ʺ:(muɷWGOްTp$xL>̎Qv$ ٺ3H垺m8VH :v|;vrxQuEb[VV>E) mb?n#m9 $!QFmtUۛpd3,iG}^1,S{%F)˧Cx[;)Y$ZNk4=|TytQoTå35{^iJYY4PPÔ)ZPos@[[W B3 )x{_qsBd⏖0Lìʧ[: c L?]8˪_=ކVcmyl eRj6c_6bc#,~ ܩyx8 =o #|i#yEI3v +dl2dLLm9@'yJ?u/f+ͤ3f)SR::7*7t,z?^JjdZ6 nd2 O{耫D+׾[YV)+Ùݼr+ ~v'c:|71jeW~oйRp9-b ^*~$5{ǩproGr+rBa!G.Qw^ekMҹV ~tzoק@jK?g9Ҿ~+rB~/hBcdObQ6Tq KbFO;d|!ysP./ BbaZ06G46WV诼$feݳ(&o'ҙ<%m;g(>u; n*7y /1t|9s48=sX/yd-0ml>2'il觯We4AF,e3E^{K^Q'qŤj_x̶h῾㿰"G&~2 5M2?b8\ }mUTa?"CAҹhPrd/@ a2Ie$]  p@YwESuصAYL{ӡU<ȫo{~sV⩫J܅̛ZLF"xNֱ{[ַ1,3&pJҟU)u7vĉDgg'qqq=nj?[q bSvp>-c%W {l_!7+gs"]4w4Sw@;//s֮uO Jgɔ|va?{h9_(ci~c+ fz]v׋nWҷvuըo݉U _b#X?X^Dݩ:ԟ=?SNQzY_QfJ(D5M,]\Dz7x(d)#@{{;VYu dd|e~zjj?Siw* +թr >蛫Cr)JJr~dFBARR:W-#w1!{UGX]z`1^ {{TZ[[9|0()} #l9u]Ruذ~P B0SsRRO7jޠ8--畔ΧN f<Ӓ%_:~z'Bbr+CUZN@:#kaWyx./Q|yٓ'Orqۻ%KUBC{2\>xocMgo!4-ta"ƓU7`OP籺z5_TRsyz?SϚ5|ŷZ*tJjj:JJJJJJ_ ^֫JKpjNVW?EQzYYX˱L5 585')URRRRRRRR X@,DR" Uiք *UJE_yu0z*~֙?l%+xrGKT9'gz)wC߭t@6 +Lkٰۯ^F].ʋ)UJjjvp2S^DQFY+XyvHJN ySRٵn4R̤TCHy7زv=U}k˘;) {MQJU1w14yMa]OI42'c5iV܇پMNz{,kڊ9d ŷ3= Ry:̻T%JVl"s'=O 4QV$Τ+~N@O[gRD׵&>]~7nIb:HI@S: *8.FX+Ŕ)Lɛr^w.w"Zϓ듸餅}Mν8?a|fq%L kl/E%u+J͒8Tڊ5l]JiZŚܾ<@,P:R\4w-]k >J)X]onpߑw-NWcgQۻj4ƔQ<$\n41;-'kQ?Qߙ̢`i|&z esJHӠ~So/"sf{E$Xi')rt"?^CEʏr3dVj7nlR'0{D4s-k6Qۦ3)>מ͠.:δpعLL׺·&}}je~8DVR 2kr$O`mpfMT=kTxV+`ݥJhi=BmY7_]IMR ndR[Kk,.-]nZMeqxess-C+fO|~Aн=ƔgR2e9 Gؾt&M`u ~ & LcٷqfԴhh>H?ksQ)v_6מJj[@+͒cQ՚Q_=Kr+A 5߫ZwVqS~x?%)pb ^}9?tWľ>hm$HHN;N%g0$ chۇ= _byiNr1N{u%g3A@}''}FB;(3A67_!g'wI\^ E(cݡLNW.^~W̽l>C;m*Vr\t@ j/-"I2)㐣a-tĪy~)( XA'Q(yAܸ`:YO>%,pi\O Ee*$hݐYDo#u$^+qrT8 1Q{'(3#|!N귮&v\_=+^&vUl{:q6p:io`,BtSY_5{IgRcA)IZVv>VDwviLN7k~>Yg ʾdVq$|Ԯ{+k]n9(@ 4Q1:w3Z@ye~n n+Fڧ}Moy+nL#yUO(%lMc3ɒ>N|v 귾IMm5fNy|8\231B4ð{7eV4-m4Z oਈfh^4;<npi jjZӊom9e(ijxǔXBZ?~)8u4) TpR2dͿR5˨*II~w5)驣7>LOHdݵ3>LiAh_CG|:17ɇ Tlj@ŵZ9HphHJGT$hmlc.3t e'p\FA#S?rrcjN|-C(:$嫏vH 3d8)|z+(dKNF;~Gґ23%&W{\AMY˰ĉo_+ΟQ珒҅|b]`5i Zh0HòAQhȑw)q؃V-9Ըr rN&f$>.. Wh9|Q=e2" 4~RʮRJ6j{Y}ؓ*/Uk2c& ȓ4},3ryȑ!.W=G\X )Ϭ;BnYnC_iBګ^zG3Gˀ]iJ<?S%)}Ec Mlqgr[ɚq?eEkv8~j ߡ)}կ>R2+ϔ:Vy0d;_P)V3goZwǦ,v\AΊ盙̎rjXq 䅍4/cey<8߾^GRjRh?ݴڋ`L&Nxand&w!}j1>k.% ܾ(ce擌vp@uQtl,WbMhk ^.1^XXlm;MF[ p)<x®K fw @a$t+8=q!.ZN[ނN"㊄Gݛ"&$v|.{A؆_MV%\)'ZIMOGu@%ćsg0Nq>Ѣ+. * ؜ ©5IDAT>qݍ".~LrJ1~ٟ 'j=!^8l6B*CH)m!ç[C]>y%Niaה3k&!C M/Rh".+p.A!M ]jX[FBtڄ&4[33k]rr$Gi> u]>~_ !M6u ߧy `:Mtd seI{(/@t#ZJV[܀!Zׯ5iCq9wqZcJ=R'i 5 ܾx:9:nr/IW-ީf:AθۧTR‚lo#N + *Uڻwo1cɀ&Ѵ`%ҁ.nh WrqNtN~dR]-yNB'ψ)<'9њ 9l69HNLɅ>;bBn!\t^%Skঢ়̌8'dJlvˮv-X^6!v(8 BA"2rUL+4!T*9h3cq!n#a·pO(d6]eӴ?.R r% [N ݖ._mOI27.[d_1fSrC;ۧ h!KJ)iUJ!SʔCG!NyP;Dm{^7DYXaέݕ%5+ovc;]*&ek.7nj6*GBoCw\~y' 4vr }K[s SVQeau]>ߏN>?׾6i|6>sڹqԳ6]Ak$N{S 맓6E`zO"7'ZXmH^M,\/5ZEVj+[>~ƃU\2wQ0[l.bUi߀M_QAY?$?兟=H! OXDy_}Կa7ہT]x+]xre͎,Q@6`pj*)}gE7ȄS'9o/ \ ut& ݎkx],' W>>Ɛ"/YЄf -'(P$?Cdvh XM AR!WL;;H35_w7C_9E#NEpZsX. q"ib+NO[ϛXqE4횐îi,j! ç'b] [!X!bؼم,apjȺv66-ǏeSeQyլkƷWW>Ԥ;Űi7\Mc/_0tueIB n.mRJ&+W}!6(%UO͒N)6ý}P 2v3 m]HRɛJ6!ICɆ7^/B˸aS-Gt׮qZnKS?Jj (Q:W=)6IR[#tu] C/6s][h%]o{82;gqm8m}gm')q(73qh/VRx7IXeִy\؎SBRa)s{i&fZ˟2J)l ڣ{"4˥`FڨA41a(,{;̪UI=iLy1y_qtR~D 4fB>< /敧`\L٭AW:}.,ir.*U4y^댌e1-)))bO.a 7ѱ5F3k ? \h>(bi_G)^_x4 m#u]u]0vmK|3%eYprCoB3Ԋo\]\2X]a?l26oa<a\F3Auo֮]Sl_|ȝ6yߛA뻯B)cUqddu#a)]ڱcG 7\?sLLzc>T]O|||׸ш/`krl͟ci0Ty> L  n@*LjM2V`KBP+ @) * e&(u,#Ѵ4*B BgM3H[A.n_kDhh6WE?cټ#xk5/b5(ɬcOe)WX\%d0VU%o9~eE?dȐy OLL4;ʱ h{۰#k7]igt`"V! 0&xN03=F P3k[+k,vY m:o,@v96?G${]G{vflE~8X*3!ɤZWo|͈n-Z0:Qf6쮆e 7u=l{ EZLpd-Z3Z4 Xŕ55O7g_1>ȇ7ѱu= 01\655ǵeSteF5ci6VA3̞YЀp+Q R}Z "^#V3Ԛ6ݼl(4mȆ燡a8 /c ia̓kT+6\N0燶˝ VXp]R@tAA/0mA))Њ+"f#z[c]3Z9V ˙41`oB̦^Me#YlP%*z5ڧ0J 0?uZp˹5¬Ҕ G!,Y͍V>+b͟ A8ݚa]JJJJd,J2JJJw]ĵ_ǵU&??_נ96|.527ZZ3ZB r`j11)>P k< E?NZ%%sŋ~^ z@l#`kʚV@KONX hOo#WKxglnDٰ󊵓* 0gq0é0v%o1! 氅h0k w`,!`3ymvfs&ah(fpF66!!v|IߏIHH 3}(oyZAm{(0h ;5pjӁ%rv9DZ4s؀UOt],FZƬ .`~C#z$zBp*P]'O̙3_!C!A@;^tarj*]U@ hp_5m,=O`Q ۨzhNmZFb}[Ze~D\Lk(D*Dz: Ƅum8dkk,oN!Z=`uR{IW:aâÁ 34xAP%%iΝ}E{{i}m^oD ;n1nm Z253 ŬnҚVr6Z}Y9Vg2! :d`РAՍ7Uvimm,?iҤk:a5:ួtJ dN_o l`F>c-QXvA[k Z:@7f,rk4 HnvA &B8@hkn:Ettty M+u(S\OӮA^U @VIIBm{G_?ejc6ڱhU z{Dvknlf`*@ZkWXA f=BBA`St~V`ـYzR@IIT}[H϶±ZXe:wk"׼c~<Pa!܉%  4s+u\_!!ǶGJ0uF4Bu@z$HX TΘ7.뺇c!\0D{ GZ"*z:gUWIl [+`գ|Bq}?Ƕ7Hf3)@h=k-:qb`\]GBwFFd3ƿ =;>1Zc>\2Va7zܭUZ- :;B !8B V0YI5RRR:ۘlm_~Y'lB2?3Čź^03wk٘˴ D#Y!zگi2_!x_}Rt 3PR t7e$Ff2Gu9.Lo匙ƒ1WT}x5R ΙTh҅O*(a{؎v= ~l2bV}-@҅~BVRRRRR@޺%H9l_|8>̆oiV!fUP@VIIIIIAn3;qqa fU!J dTeqOD[NqG]AY%%%%%U_ `6˴s 竮>uUn*@I?-sz?Vb*)URRRRRy[Gשc- b*)))))z\uMgv>- b*)))))]:Nߖ:>},@VIIIIIAK `*)))))]ĺO՝ZJ dauᷩ8OII]\JJJJJJ^`TVI]JJJJJJJTh @IENDB`icingadb-web-1.4.0/doc/res/service-detail-preview.png000066400000000000000000004561741516124654200225010ustar00rootroot00000000000000PNG  IHDR_}j zTXtRaw profile type exifxڵgv$;vcZY9ځ/EyH2a>*)DSl-6yQog}uuZ%fk~Ϋ:_ooy뷆'hDz^ WC?oWͭSz1?Fb:_/Xo% @Џ3[o3`{J NӨw~ﯾ9uÅҷ5|u?>sV5gv=fL_z}ōXx]wwe|OלE\w0/~(]g/E} Z̉{WXtwzGc'ѯo:GuNJ !7ww(]}_L̕ v;&FrRO^ `"N fsرNCb>qNg`H!kH&|%`#~JPO!ŔRN%R!ǜr% z %Tr)Vz 5Ts-jo1Jr ?ˆ#<ʨ> gyYl/'V^eVnJ;.!N8O9߽M;55=˻x&$gxGNj<@@{V7r|f'+gIYNÃq;{݇oͿyu 7o?xmP4PFqlWk2K,uHmHē EN.9r vƈĵSX,^Bkgd⬁}eb"W Lo9y,֚b 𚕢1玥G`&׸H@uӾ۽O;y9U?{%#wg%ޙ6}il\rZM;aO,;4Edvɏ3}߇BMl>CI5[EH02f+yb@<(_EaydQUCO{#zp;F&G:E9w0gr,f -S`XA&s=cp;zSoKTDvUf 9G:~ xZm02=ߦ ~1#cH&>y< q. CN ӓ#OʷY}乮5b0M`C;YbE2)љ~3cZrwPؤ8HN|kC{oFŐJ\M5~@`TS͗ɽ65?,~DAv!fر\ 5U*Xry P@>k 9#b :ډX:Ks\R\^iŰz Yb]'w*+3{hOXc׮͟&nff S|1rFIe$_Ɨ)5N; :;U\R)@xZ5&d ȍ)^cvƳaLXQA>f 0L}n@9nOl T"jUӽb6IJK.[kq(q}QL2U!d(TlL1,/DF1>6;IBAISˌ,j[TIJd071.)ڃ8DbtHOTJe.)N Mx{lG⺙%YsXj)QJ4vk`+ BbL<#=F~p) <] ) +9WY}83|cOPr%J< O7Q"55f{]z= ᜉI1CXtHFbWf8!!S1G^;}d[bppz!>B׼dH'j 93WR^} _h$0<)[iЩP|1Mf/S~m\Lh*$KsgOmbC`I>DqNt̡J(!HI$ a~0E'!hX/,p6hSZL196VFQ[ܸ7BdqS0-fh_X؁t)1-EO958@:ws`Dr ۭV &q\<pM _Sz"$ypHl,21i f(UP#lOF4h2#;9RSG;x+ ܇ C Ndب3Ht=xZzUݏsR!>6;h!c$yiB2h\: i,/ɕpMrs )@ WD!QHt;U6{z Ԃ PQL0)bs@B%BOAD*"†#HBW)sI4tJ_I gi vxVAg㛪N%$`h6R#J. /fUIWyb}j.QZmD0RSYkK ?M?2x2_2c.O`I]G+pSeLYJMDswq 7z@qk8t("iO/jpxJl*r{$0=A됎 B,G]wurtrJF!*8A^^]9*@~#ZȣC (B||VD"f ]͆ &H`QI? -0ƥ;2h[Z&A8TlMB  Wl) ¤#bo&Z\fT Ӷ@V!;")T$h^p9J cjp>xMRUӌ6t$n*UE *ġE!D_XKe8I]aX ]tkA[$^ D}3 BO:@&R;ŏh>x{97L ©>JDUt&5X` ʠ_ ]ţؼWUa^g?^WAMd2TUz@cC"e0Ng29QC1"hYըڔT!*/L\(4#k]+`z$*H>UChˌ Q$)0\&W!&1:y*H.1AJ6Y!MVɵ=3D9-o$F-gToxw0rFX@1#OڗRlr %kR@7Jusjm_. S-+]UjlDpRQ?~sX6:Jn|uL<@AcpK%DNm p K%l;+rzn z.]FMFte$vOcuҰ BB@z DjA+O}mlĕ9IeK wwHk%qZ|^_B3VA3ym>7ZCzt?!vnh A[z7+y 'wMuH5 #s+6 1'մq$խnDC A}A=I7CGU۫7fJ>k䦒BvQiՂ-Vk ]1<ۚG&}/NGe\ 8Zh|zSK/T%(قi_ $&bXڣADm7=XFZqPPFBܒn1YO!*P&~X.X.r=zy^vTvY鑡3KoqͅygnQ˭w5Q@FmhH2I2\_hwȬ#ags@BvG1y}<噩̦RnhW/,_ H"_|6ɷIP'ѡRs3vkpS)nxސq…MjNv?)漇+F#Fw\]G4tf?WjƮQ1-9t:괢N]{X;^g yL 7g Y$_ *Z0fBV ,ʝ }wk[F3̍Qm9mh*:q-X}@rFu|Nx2YW4q<,1Q@dn\:OºH>h"0D1t觽?] %dӡG4eG9P6G:SB.nԈ-0<ȀT';*;}R`%aCj[hE'ucEV҂MG-mu{9PNmV~8σ0饌]OY$93EIx!S̄A2OM<-&RQֺiga5wU~g5LIGI9"g&EyNH[:q "SP5uekE5Ť`îQ=gگ]q{yt;5,xoZacKU(WX =36x̝7 -(W=^Cx ~<м!`gCQHWW{da?5~6{kl 7ӲIy|^R< pG@!W34K*G=4]0'|ezTXtRaw profile type iptcx=J1@yOJNy΅sps2ZZ亟CpG [cp(|Q۠ۓa" F?tTl۷4 iTXtXML:com.adobe.xmp E[riCCPICC profile(}=HPO[T ␡:Yq*BZu0y4iHR\ׂ?Ug]\AIEJ/)}TkP5H'B6*_Bj!(๾]gys*y>xEA #ymeTHbK!@F eT`!NF4's{tYэ*$| B!Bܨ2(7sh˿uvCѿ ɞm~v,B!B|Wga;^` ˞<< B!BՆ喽ʍL^*42ӁB!{_2;{ֲfs.'jݐ1$<1/||ˈe!X5_h3q=?}?~u;sEŝG8î՚*:;}:}Z;qAk﻾̼{wB8קyI}$$B!A0Xğ.)r掂wb+oWdF3뗱խ7-gO#VAYiPY꾍*xax>Lґ:M Oè*`7xCX}(rep _9 >n)$B!h\lxfL|ȯ=a/xCsG[YVE uKe9վkHab^_E f׭b=U'0=Y5w+ʟ)XOGy? }B!UM-%y^oٍzhuX]& JG`+@[yf%-{:(0S1p8ekA>'fL˞73TRW/%)ʡ2N0w)lTwaT'<8CÙzMW O:4QqOoU+S5a8_6ġgnϱ ~ʛUv;VWq@hxa,ߗSmq޴G *:L!66HT!B!͟T|nNXkg. 1qB9kH})cwprdmt @9{1&NLk"4V/cZ@Tb**j~t-Is7\ZBb&N^KI}#f1W% s{P)G[,T+8ui;@ӽM5f՟ó7hqsb} ( wWحVցҲ=F[" ۥZJwMTyx׊W63f7P?ue{f泮fMCݣog BDB!yzs#_37|Vb"]ox 0aw'^|SӖl XWXyOP2$p 7vh8. V`ܦCK&2E1n ԈKI}Ic114/[V2P<=ۨ5l+u}}_չu8zC >)x{:6]Je(X 4^ͳ)'Y[F[u"41*-:~D>ɶ\^9g&f!B:,_ [VM=͸'uÇdKvr-9oъi. WecjSq*>.oeCc0^ ME di*h&'e UɠrȮtQZejMё,GrD46A%Psۈ ^pYO'nTx0v=AߎFuyNS?w<_AՁw/ @ۈ V?VzpOA+>' B!Zawy9,7gSjqOa7z{k;p3.&xR8|YL ݛؙNB!hӐxo.Ou(K%*?ɻǮǵf'hge{`T@Ox];o~$DAQAz!Λ9cBZFn]OleПƣ%|g} eGY ۶UL[I?{NH4!>P{)PjvfmI\]2/-Ә9Joql*%0M`GVlSMN,gJ9{inlcGuIq k[!H|0NrWKF}]aJ?J'h-N@;8G4~ڮB7"sЗm`D/EH&4QTmʦ'aQ\j:Ơ%`2v"eK!FHf1A]1LJYblAܟ l"d6|V фOa8p)GBy-.o7xÜTv#;a:E?/@쓋HKG,) 2RRa04k$%)X)WQca0ggƠ֯_nЦ%@C~^CBj4a?58 >\̋_dE|~>q]YTT,*aa\\_e ˭UU6Cb;(S9U+ eD@WYy$Ͼymύbs:E 8EaDXo`ꭔ\&xez/':k.ơ yk68Ĵ'̂JMVỌ18U rjZDY#J()Boep9~3'kɫ@\@;%w9;0CɖMbLsg[6q!/8NIX{k 4U8~Zl~?g :`4J@IV\U)V(=ZIY&2 mhul$/$T6) !NcRu> }܆'d(b26fS__M^(NPM$Ԁl>RSM&6?rbŢ1XW@q`Kup:a ߘv>g1y"qm^#S$r-,Ng fωF6qR_ٔց"tswZ 6r/%~pqɉ }FTzp5ϝ?_Σxy3p/yxZ62rLMhD(G5R}-Pe4j8j7^00(t6 7FvđKM$/^ﱩ|犰LG ?OgfU @K(L22'aqٱaj6^ eۓPLe}AoCl5ygpǨYӈClZTroô׆ fjcYDs[2)SpvDƆp/ SPw™s "FtUIvL =V]Vn~|g(8Qw`bLwbG۠VRuUV-ALHG ֥9/cuTZMj*ctTI(G7P߫,Z'K0E税>/:9u49`w`r)IG lSj8'01TvGMlym/AMt}'zg_j>leݒAA<5w_eޑH5M8]S$fSyYɁt'Q5dFlBxp^ C>Y56_57Maނ=s)Gm;HYofKLcfEl`Mc=|})|s!v0ao3h~2sF(1m|%1O\6kWr*Av XH{n/#8j ,XC<cb%lGw`+)/:z@p~I13d R=d9+g0[΢3&r-.6保,ZHd6 Mao:w y+IĆ JWa7e*3ZI+\k)Ré ^X q@VW3Eb:m$hJH͋<(o) ]U=&r T[Sv < Bȧ7IJJE:p>ȣnĨjsg:ž ̝Gm/#+u.dΚ8;a.ʜ|bǵNYqUqٺ/e l6#kٺzC *{BI?fo^ElXN@SP T +9o֒$g{I~SHFvb.$_˫Q~{WE#5ʋE%5*)) 0G"LG3Bʾ*Pn IDAT B[U^6yZ ϬEg a],_^lB9"T#Z Razp$͔NLTe2u%<㞞AmhYfićtOM8;Y4I-NJN֧$Q?ЩghMǒDZ`Z(&3 fe{.spS6 q)p(5c09Oe:pVйDfU(~F8SQ4:);b"(~t6U[i>A3沠ZC< 6^VgN|JhA1`_si3cKI_Ƅ@{5M:chqhG3s`˧n(fF5-g^YX :nUų K} 5u^nՄqFQXGQ;KIu/GF҂ndNo"-{ $rxk>*$tEc %rX&+7!h1 tZrz23'+eyj7D#q32T&ɻ4lQN Aa7F&e\orps !(l8ko7miUBO$}nxeX/&w1+(uVɲL s_Hc?2aꃇ?J6k=:98@ЌiĪ] $vpOMB?blLx􊎷}4ُ)™!p*L(P<߆d,`Ҁ6ǍDAc='R.,O3kdhtBʇۣ`s 3Ah:CB|ZaAL2~=]|ɵ0)BKL M!Sm2}\=KAX2C vP'M ]o⺌",Ӵj[i(ԻkO7r aol\:MC5&ܥ"3BQ}BJ|'VIo;E&izWaET ԣàStn: _W@GJ- _ 3DGC̥q4*_KrGal wNNIߵ@OW ♃[QzP`a݉Jz><:|l,MQr^F_,Ƭ07pC`Taf뿊(Eߟ={ 0 Os&i(hTc՞<**0WQua~=Lkg1115Q(V|7Rh%g"gSFT1l|v ftⰣ M|<&ZCi(-'^掖[hbkX<d Z݆W_T(`5u<4S_C}LHŻ"x7pmP{Q*tŠ`j?JcSy8Cùh eB q- !4L~z _5 6NyZ'~o: EEHd4XJ3I{AO'z=ߞ 0%͉Z{BE`J;u;tvysZ+R[]Neg̜L㋹~W|^~tQj"t8g@*#Ӥ$|{:0%50Y|LÙ ,7;Oj4>e *,R[=e7mf(xX ?t,uJH{3&80_xy{M!%²<k^jrP J=.=:}o-qxj4vbv /05mkM=I,BެAVu[>ViЪ뱶fje95:Pef$xo+jMqc]Z J_vob{y5 Jځj%IqgqKGۧ[DV:*1~ywlf y.ȇf1`Vz_~֛v;IV[] L@,6Cpbͩjpat*PCج@c M BdwѩlWPx[e${}<FSbp6KE\kyKm.Xǰ7k\dZԶJs=& 6> 4]0߹AIsuFϰI̾Rt62ԘLڜb&͙DhW '?Ik]/Pp+ WRKmh~u/oMvC5i(eu8-52Xk*3v=ŐixzֺF: O3ȶDMed<(":gܕ>Db^K)/L]6UNWvX `*-wY8fpuMgª MP;@*J NYVf޶+֮sRAֻϋ年;ZO!LUGucuVN fJ7HqtB[E)bV,k>`px v'j%Z- [J#󼁩ӣ]C !zz eT M6{߸~k׀hxh8.U^JT妜L=kXU̓/-g.g/® L99T1e'dv ׺l2VwaX r(l{)МKW"tͩrحXޗ;> nI*ݏ4R2v\u\䥬%SFڄx 4>ZVk7۴4F?+W/0*vHp3s FGg G|rx)>^{4j_~3rzB5^*zjoubxu V=.Uwcg>W33F;nv]ڴ*>itF`4m.I=M|p۶rTjJPr^a ZJ (:38=y(w_zq_>)i\&'V_Yńs-t-}7SFC竽}{/tT@1zQX-4PYik4pEom6ׇD{l6+zNjJJ179\n@Rfw5*fr hSr5"Ymi-8tZq ;ñxbX-!^ZS ;b4vU t =Xl#|RooC' 86anr[M'k fH<}m}P+5!WS='s:,LݒEq/0祑BWy+|*nh9mxC 8ˋ1YZ_0Rx=(V^2?F}X#0}͑]nC=Gy(u7D3Ua'fe*B+:t>4F{ϸyӟs]dq;ۼOAРjJ/JU h8ZP2*/pZŵ\{eg/!Gv^(&kfmsw>ͩntb9oҤ۟_>˳u?=>fDKT2?g2#ZKW$8\T.ͫX:(ȪŬ57f*-.8/o~S_vo{CxpEl{qutcWE },!#d ˻/cS.N4A$`@N@CD_PE᮵R$G+vS\PS`X;\M~;/*9yOaPa={olIp I/$3+1SO=#.$~Mc9oo=uV6\R'g P? ڄ1;m= ՛ّg"A=HfSC\TDf:T-GoKÒfvUL>Üt VT'КOg8ڻ/BUC@I-kH2@H"}6y]%~:||?-nLKq̼/E/#j ݏt(lHGO$~X:Q]$F>w]^?o`~[hwЌÙXod?Eǰ4V(F?a2!]|<ʨ-quoX,t2 r脟JXЯmrh2v6&Y3 WSVHlL:w*Fc^lH!خIcC.ퟮK=bOgmJ1Z gq<ꞰU7,mZCgN>/:]TV`ڶCZ,=.c9zVVC>pL֟ǂQtd%w<@u& ! q(ʳYL v~MݰD_`B*CZO4GDiQWrAj_aOWǰ6Pȡ:hg$C]˲~ >$ROBèz-oy w]/ŋ-ni'{&:Waؚϒ]΢U_˒37+'(E _=Y-V hg,,%T?z/Uưyϴ<^h0k,T>ˣ!q˚wy`~g| ޭdn}D ,ٶNڏ&Pe~[.͒݀%1?քˁ՘F-d֥<k#g#8ho%d5'd31C tD~>ҲUk eSPgQ>e' m3|_Mu\7ݗޜxm7'0rŽk.fe5`\?xOXBZ*T[tM~&6,J,mp1Kܪ>\R9݊٪DS='747ĠA!?LMlΫE2G`?#Q{5'm_o`)S%xGO=} rҟ=s(JXwEP*{A0֑%*OO vo G7n(dG#ˍ&6,JE+/!?c+VG ,B`); 0Ǔ$aĊ7\+),1{7gnAOW{A߯`1gmmu6.bvC_7gOoMgmM̍&s[$<GwR;%6p"f> =$<3>'KXYŠ!@B!Ŀ/ux,A&6\W ?K7eHbTvó}~YTIB!Bѽ{Coyh9RAƆSBۿ B!B-mܞ~B!BR<B!B B!B!f!B!BB!B!Y!B!,B!BH`B!B!$0 !B!B!B B!B!Fib[ĢZ̎^jJB̜ݣI)B!B[33=g7JdRT@I4I|xm+o8V; $=CS*WEO|60}t [I7d|Q6? 4;h`5Xjao#{%Gz6u0ډ8>g0F}A_Rfϲ.825F.;,NBGe>^7ymkp`2>bmR;l8GވSgdg'X{ =x1`֒'D;Bs^3oRl"3gp"C8{ Mlx)ެusm:˺cfFPI T 9D r4h Md+fd2H4)W 6Ճv;gPד_AKXTo K"B!9vWZ|3!)5ܶ|%W`Qr+14+j4i\n}'?0^dm$6p=5&lh ̢ϵߙm˗/FV üTf=5\gx+C/Xp! _Aّ|#_7Q]VEurHz +˹Ir.L~K!2m̲-Vۗ f-Y¬3Кngw1dv/=u9'hTnnJ5Z(1c@Sb~ 0NEh0_KXQq;I˃7Υ8yVgh@=j[a}wsޕ,[5$L:Y-,o<Tݶ.rjg/??ZʬMh!B! aaӀ|7 q}'<8|9oIB!B!D0peaB!B 9ì"B!B!NvjmrWk!B!ąI49F>u/:::pkF׎yg=v VTZ>9rWk!B!ėǎhz޾|mkd_ iIH$Q}tQGqSeH0^#9 wڮ2Mh'21IB5dTƧP9R }je2&8/ CDJ6 4h2&Ao\sAĉhL4Az!B![S_Adc5'gqph7x }R&8QQf'8xrM,V'+ O%Z*lhGѧ@?& #3.\~=Y${R>[MEvrDI~smmS.#ӨNNj< \ mi^08L}U;6O j7^" u5x;Br~>Jbsj@7 T7{ʅG4-:I1OX,Rxi7WjO$3xИrHwX,}UڼՃ \2.B!W'")X-t5y!&ji gA"34M#蒒nkǐ!I`Ykm[HlfAc`irǑN}C]5x](b8Xh:"a?tr{P%]?a 4uI͟HƚNȜx.s9Z料ˀOAсImv{ r0F%h ,Ʃ$RgLVf qQDhZF8Ba^GϋPa**5DZB!_9  sFtt0=S*ɑ="xtps@#AV`$M6qV^D:Bsrмg$Qvop ~;>p%aCnB!B|hjO#~5h6 g{mN5Q'馷Oa*d H=u:<]v\}=_4rfBCC;<`rչaVPsP*OM8#(ucOU> -Ϟn#N?MtS_9aIc|U/nU@q(uXtE6ppD!!)?䣈;B!B|hMļ vO$Aā82F mQ(ɚnYuJ մtc2T*pETkAi"ALt*G_n,j:o:iucj0Qc2VRx up:H c$ѿ+W`V8Y\ 肏-!B!8%}.^:pؐGVM#0 >\>O`Z#0P [G!B!.0X:@ )3 KaJ赲E“Ȍ00 !B!R:fa`!B!bB!B B!B!f!B!BB!B!Y!B!,B!BH`B!B!$0 !B!B!B #uPuaݧllp.G'e!.(6 6EGT[܊3T׳#s*'UKB3Au?оX7ZJI@ǜkǑ.sn~+72?&E/%4tJ)&L*451]sC ݋'/cfBqQfw._:g_sJ cSuƗ Z';YqMym{xYn#SSO@^_ojtE[*)օ=ScRk-tq(4$tS+wpk/^g:? ̝LC5lTXp{1ӣUֽ9s`WU48biS\~0%= p2Ҹt,ڋ`vQ 'FO<6<M<S^ʽ \')yyLR\ |׹*0xcc*N=7L~4Xwm9kO~<߯ >G(VP0cL\r:woeoIP^хó/ns[ y$C^J!N%s7,(yt1>&x6NPWMxyY,Q"̦X6FӐg_+0&?!nɻW[X{`".A}lO9lZhPxy &gLwG WC]؁jG}5Olpڥ<=!O;wY*R jJuIGݧ]O}pf?Yo^`G]Y?~s-?tWCNifw>FCg_/[Ds% ҕ6ƞ^JtΦ <: N-%[dLS@ޏ8MEڏ+yw1Dz0F8/.,!܀}K]{IA0G^Fω:V`H6g/sOIeǝ&cupw`V^̾4/B;0]pn;[;a(\렇x4Zu< 4LKwPځӅ&+5|١k/$%8nvA aB&?gvPtCqEQ,5jY} "f%BsU-"T~R{xrXPr^X铸.h*#.}z̺ WxnwdGBj#??2Ef>^p c]RD'oN;ٸ "6vⵦ FiE(b<)Mw8h'!_Q#5=V:-9Z;/n39l''PFj< & =G:tz.KG(J`逰U5ՉCkd0dكu?>jd2#n?Y10 %fb:kJ`~rUhpaZ KrԟgŔF^",k/j̼|u5pK,xnu)&p 0iO^Ҷ?2=EA7(2X~ NO2Nie`X7а;ғNa*.dbٗReu {nr>p'G ~0' ~aΕZ=6餹EJq!!d9ڲR2 E3݉}QݶVvf#̹&!J{JCX5T_~UH0:zFʪHw'133tCJ+ ,C7ٛ+MOߕam& D*Y`\b qܢNfqy҅|~!i_aO)7&k%R$~Fw7G?t__ BVɐͻ0$ebk~=|lX~R?uT<}$ jSx`mDbRKyt.fǣww?䵄+%Sl|$%3 }?O|pܐ]<o;Cnz6?*WRx f^iGwM"cekYV!Vu)W7z[;vϳVa'e anaպVHV d.syt{UQoC.6Op\V̊==9@M?ǾUFa90c`mm`w2=ڍhLw]"FRF~c7f}FCd<ƝxCIApc`'~,R4O)ᡩjuY,_ͧzM.ouw;=tnW)FPo>s3;L7z'mj ́j$ SOf=:l]g[bx{Hukwvw0s8`1\!xgxinhaKS)#Pg VǤ$ ? jYj;2IxUT]MNjN u1C]8xtNv&~k:In1sԇy]?e7go[h".Tu},{%:uׂۇ?[X[i}XXT?t[wPs$`8lት ,ek?˓tM,2 :,@LO=UqÆcELMu{B{%MsCԩI~)W_CJmyʆ5 ؇C'7TZ᧮MC+%]QKGMNEq9DU Zf_~3z3zw, tI#߭3})bcz;&D8ݸlQP81x&4i4>+u^kcg ::hHLfzPb$tk5'ް6r,}d=n|$!3)03;JhGwuS55c7xcI@yb8z\rN 8ӮaD -wQס'dg/ .P{K'(+ٙ.RғKt2W p:Cc"8z`;DUO$-Ci1̹wR4dUz<4oRQ98Pa3$qUPa߆vW gh%)Evֆ޷ Y\lam Elx\9^yHEM ?#;dt3+ox Z!׷3T/p8u00?muD؇3c IDATɟIʾl8a $+!D5GF?[]d_ʊ[ƓTQdTغ Y/}gLCU903j*Z/.RIʿ(+OK-ѱd;No$+/MRWEUG;?n6p 4Q*rఁR/D8^5Q->Dqu^M{Xl<'T>|˷x;L n'z "h4;:e!T3`&HT|O5b!,;-Eg/"!ӃCAZ"FczyWVJkngʎOܾ{S:f^Wx-3˭I{sc?-C #UhZCD fcsI?i1U}$-ҝ41sփS=N_0)T󬥓¾21 MZoH29euS+$]/vڣZ룻ؕte{l[t)t,Y> udLHwPƓ BE.g]lh>.~ O ÆEFv5L.),f%xYx^aqԁ`Fu=d/PϫkOs՗JA;|B\P {JZ9ҋ3)jg]PC{]Klr4bKs͈\bhR2!ƑÁWGr dc]8P7.=j8ll,O'%)^y?V)"'k0%ǀ".6v=6)=XI'ռÐNGݶԤio |V>np3(ӽ!ֽNKT(xYcU$IGV=`Ng<=KUU}]bه!R96 YVx69]aNOx3e1Z^H1fDq5im* /zCvHO]<,c0tvR]Gu81jup$pc < Ig󨢞;jy*I;'"؇؆!JÜoܷŏ&&פb?3qǴV~15!OZxpX?gf6«r6|g$ll c(pЁ#%c9q;*.-G{X?w!",~ty&j?8ӣCKDqLޜЅrW$[yF>߅58ajxƆbJ1ILϋ?b_dѦ((~SCsQ O_)7GryqK !9 aa̱9yo#0w8O3$rsA0ٗ%IX>g?u<07t:)I/H4z+69'1ƣ`)eMr&K9 KxTB7K$,6@C8 ˿ǂ]8*'&J2B+ps>fŗM5mpBEBF&^4 !B!9,Y!B!A !B!,B!BH`B!B!$0 !B!B!B B!B!f!B!BB!B!Y!B!,B!B\DT&(B!_RF.bʐ B!B\VXr-B!BH`B!B!$0 !B!B!B B!B!. VVepH`B!B!aԲfg?%0 !B!8尼'%0 !B!8Kx`b8/Yh,B!b|T~f>UĭQ(`h6nMUB!B+rcP ~+LNc5n\ IB!B|?; pQQڰ;QHɗk)B!B!NawB!BBX=xP>!@h^~ZYB!BQѰgk{a 偛rz44[ff!B!_)J"bt x*d)БiXGtXO% tGD__ߖQ}b/}&5L9SumyfuO9g˟l?|T`ncސBNQ)7~{6qk !B!.6 89]#hN ;Z /ްi@ >\>8!΀!N;M6f|qBœ8/maSKx lbѫaP,ϥU{ ?yeR9cm"!B!NseMGXXEJ)3w6^|6ZNsO9뫐sp&{gWRƫ|Muܫd}&@+m [!B!DئTzQgdy^Ry| )̚HF ^S"w͛Ǽyct7C7o><>,.nvyhcޭyxͶng޼y[s ˛,5XӅul =`ÇM^¢o2>k.w>>̛w̱cnǼ[V3{󖾃+}]_2o<=}hXh\X(Å/c5փ{̻)0!B!@{ƖۋF|J`i`߈CsXXJFsQ,GlkKK(s:bo"H})xY}4i'oO0aox8vZqeB!BQZ g`Vfps=/<~3]6}g黌- 'G8C޼= gύ,Ák4aA;P.pM08B!BqR\Tk%`ހw*(MWe&I g+0늙=3p wVfr4/fvI`:"&8ha{ut:N;*ɩ}d 2[QY蛎yz%J !B!QjmM&Y k^M5VI@3NοZ&/GA NpNVS8/WZakzjG N&^Z@ ŷQXGEܵbs\{73g7~Myn+e֤@(.A yդvUaq0`w4=ܖ`yf>bB!BkYSmdr:Ty(AfR+x&6`q1wef|0Ʒ֤2yndr  ǖ?~'j-%à*xn{6<98ܘo@XO3`9V`nCf)n_'~߸lL+I[\]#kỹְ/-ahqׯ 6{f.RB!6 vJaO7`ɸ|syVf΢X'G!B!оY'-_ʨi'dSy9t~Y!B!Wŗl!B!xOlB!Bq2 B!B!f!B!BB!B!Y!B!,B!BH`B!B!$0 !B!չ@ߏ'[@!B!ĐPt:s7{g{zzpB!BH__ns<0{^B!B!.J@>]W\kom`nb1zsT$^IvЯ¿o&*_?>OuhWŚZ<̈}YiQ_ݵ_<ݓbyƮYD3:+K&̋b} ~F׳l|<u ;Ȟq`3_k eUX-)iCU7Pz7^x_- ;NkRFyȈ?O~F6ȸ*N5?1W쟉\7GNV_="XMbn"&R#fYF2t WW㧅nKt jia]5A*2TN C8 $$ ?f ePl>qg9ss5j~XY 5uѢL6i57_?6^5uT;ُZP.*~ެzݡw|h״Q6 EGokSr }oVRg'D>ƫzAtq>ʅ3/hz@}d`j,2`zTPUaS s)z؊TbZ|ק =iк,#k=cu7UQ쥫Pb߰Rt wSr5#`FE%~כZJWpLd5w1E-𼼒Ճu 5} qQCEO +)[bB5y7˩NcqcEYi959Քd6Oݳo:c}f ;\4K'wݔ:ܭwW9˲-5,_:I IDAT${Y6|T?UJ Σ6\Tӭb4;t^zg0Nb|;jX,.aѵp_5x:@iqW.6CddJx2+jGPd?52QE+.GuJ> ~XI*ܐ˂-(RXT?Ϭ_Fk)zD[J)|Ʋ~ABʻ)Qo%ɽ!@Z=%,?jYLOG6l1I@kjigãхי7Qgpޝ(Xk4#`OHQ…23k|`[oT*|q_|knq{1^62نEIUE%_׭CsI Z}}F bsrȹ(g>ܲDKOzl?xZ5/w+(K L}<_YNZ,_Ь5R`,rna@l ВY4LNJ[)*j' Ƞ\LU{2QuV@4x!ǛJ*(Q4b!6 feC) LתаĨFӫpȜhqEn9J&7 DwB}s0IKu6a6hUcd>?0x N_Ѻ WR3~܊jx筿 ̞e;5n/եNlCU~$3 8p7`fԳ(?Q*ꓲYc+J(@5xZ8^Xv0<$ߺv %PGŋ>yEOTo):,icUNbq _j+HEX b*6YXryiEQP뗆ix^^Nj/׻u)gz Xdsmu1M1MgR43uۢYjvr~0̎džs"zݼ hﮤzҦc P˜Nlu< ǝfUT,~*Sʧi~5Rdzdyɞt< Pl5rIjĘr X7& c$8oń! Ңyc)j quu EQN(:OO:=<[8M3Ph&X@`5iy32kd$E,<2΢3l[oU[oン(oh8hPzbU uZ&Jϻn|A՚AMց;,!] YJ|y3|/p SF;P4 9o_VQLXk %%ENSjCI=G6M5 vvT; ( Z Fs OhX*ZH$-B'vj(mU̴N+޾z:Luh뗊)Պuv.JJXrʨ񇿧:ю4< TJ fU9Ȟkr2=ƫLyYKGQe,\;߫ J%QEQu =O x?TTK@QPwD#J]03jޥxYDLjJmj^ALqأQ7PoҡC} vfr\: -,]`M#Llvh(j% zHvrF3ZZȨa }g?fY}: 3ÍJx\OTA4Uo&L4//(U>˨A:o֠_Eabj6%"੡w+Q<$|Yޭj { >q{uHή HgX Y?΢Zʖ_b#}~S(4S`.R>N 2Ns|N22I΃:tg`\ XƝJ;3 $(Oi(p_(@vm1,2ɹ1YSQ *J娆VAKimӪi:M_Bu zzFC7]~,?h2I\kw M}AdήeyC#l ʰt,,C.oڂbz L4d;PѢ/#@wbϳsEc l:J Lkc/~ƫbޡ$p1):qKW(3`q8zPGIPQPPÓhf݋>(Z7̀i#58v1a3GA7d?Q#U9kq *A~Yg0m0AvW@ZHi=3ªg LhȞwq FL k.3bJ6Pb5l@ъ)VE97|~st/ ӝXQ^]y߉20h~B=- |rEX3=/?u40΄ \}\QP@sA"m.P-X(jMxlؒϢhީl>rR,XZY "]Iş_O{8{ zYOU2_@y7ueﬢ#?F ?3ۍ΢yǍ?Pa`p5QM>2ر}RGN n?Ձ-aل}uu[١<ÆwvK#ي/j,ڟh}_ g>=)Nr3ap>E˩71Z1ӯk1Ls,D`N'7?K,ܫ),P0c*4ꀊ4_(e#idhԌ_Ld NUT.RQ1a]6L?IR %F2/deť7`f-s6qJ?#Q4rP:w"j%QG?f٨U={s5ZnSu^tx*l;LƃT)M,ߛ$scHG?$uz˫hԉܛSJ; \BOSNv j!9ő0`uYk';OK un.SQn_F-'˨xb9& 8VRw˭ H{G+!Vpe:9&/> c9' \ 첿|!лu,˂\WbT̳cS`r.9x̞4[ʭN֭)̣*LMb *JR0LryCCNƍu?_6ÌuK!gBx ̿.gicK7rnFk3찦W#LrfnLl"n9KF^>){Tc (3;sӪ&((v2wѮD^ 37ᗟd?,\ jߟM 󂠚[@ uN=6.9?`;\~5<< { ς nRu"J+HSeԵkh})G33$r8;Ϋ,(Y}JzXر}gσ]߇eB0%`.5TǛߚ{0eJזST床yV6s Uyoʈy36Xx`TdS?^?lkk\BD'୧*'~nuRtVPCZ]:|['+^%]jq_i"ߵEj\-%c ROj~ .2-SZYxA'試:|d?- HӅTȋG;X,k]]Ab2;;m1@5qL'XU4߭XTBL lu^P 62aR) Xrg%@/SE&Eq?vDݧ_SNx#sȹL[j4n^!Lrq`W!BH m=u`zC i@~[VP0Obl Af iSmi8F{pB`&칑}XFEұb%> Yw9P}IPub£! [Aj)1L VF0ذ!bvO&hsZZWr]քW_lŚ9{)|)a/ȉ}5Td~YаuϗAt;O;<פc3 \+n !B fp"Jx* ?^r2z*7h8f Rb)U[,5LrJnq=k(û7XEX4-|3mTws,pJ4B>j~WM|5jWUb~bvVQ;=%6 /AUP1$ !I6Ҿ 0AK$6u!ք7n3f2 w P.1-V5 ˩@Wo!{@3*k⸽7a%>=˰xh1Zjvr~` 6GkqO#s/ڔ:9\`H2`8= }ՏLk?7P>o6The_(̀elad#tZshȲl!BuxWRkiF)Fmg;OmbĒXw{@Iu`v8jˍ6͉MZ=xVZi8gUE8o`E,k xB!kb"kUy豠Sh:~[CP8A{d} \;tZAQ 89ǧX:t@l)gEJJ_a]DYEMʼnQT.WTvfR+)}+<88; IDATdlP%نsIi/1a z(Ϸ~j>Q0͉Um`Y1C 7l^dN &.?4'1%@^7Γ%L:ċx6yP"PI4϶??/yNs:,ax`1E;nFօ[C)feso_GudF;aCʟhbAi7ec0\fDAŔltkـ^,B!__%QhƤt]S*XY WOc@;^T[_1ف]P gRd=oDAUS<]@Cx7Xtj `07RG:QC}pZE,=h0A`07s;OݩM:%˨z/ɞj|Sվ4}?=B`g5ox ͇C?qƳ|?3} Aϗ2sLh=4ƚ0% gK_EX o1߰ewY~VG"xy) Wd@niX]ҼzLr`떲rkѺu֡Jc,B!.<#"[CxyDH`=QM=~xP>Mr]!B!İ$%% >1 h "[ ONB!B!8B!B!B!B!B!B!B!B!B!B!B!B!B!B!?f$4%8:if'yx#͡Ǿ'H!B!@|3̡&ޞ?J;y{94q-k%Rug+B!Y,0߿Goʉ'x`(\2 0v.9WcG!8[=Hh|G3MSӂ`3 Ӯ'1?B>~z1vqn`[;i:ÏN:S/?Bd0^vo3oڹ|?cwxoO,&0og>(Y& y-G;L5q!G?]"W!B!y'tXd Ev<}/}-X|'lDs$__و:g 4׾Ė cNw꧳Hc?]Þ=~h'η>Oh8)>'G%%n7yv<3'x]1.tBxGHT~ʓ6v7c7{"<ɏ~;;hٱ!Hu;yv La!yw݌ظ&)[B!Bq\?+@` K;HIzv~o#e-J&gBbJ IBttQWINb'2I@xƛ¯84vtjP'?UsHķI+]tFyYd…W2*6l9̿fM`Y!B!η/}9$Zi ^ IG͘^ :;Sޮ$&%HG~Z`:KQ?p7oxm8;j` ~/T4ׁ+n&IRxB!*`e 4fRRN߼1ϬxDF=w{QO']`8`5Vetcǧ-nG|dy64{WomfϞYvh+B!ї$8VqWf:i~u{< qNl\VR-_E7Nu%ba>Nj2tv@i ]ɼk]wWᆻĽP+RvB!Vٽ W%.$$vL7t5s_iT£HIlųC'q\99ID]|n[}9wWxR/`w1' H(ܻ<,eߌg#7c7C i=a*B!B0lްͮN31Ysom'mNSKY[xf6&2vnq>NL7PTamlp3CqP8ލpBîff1'gkXN['610Ff!B!8NrCx`>#8 ǏK6 !B!#l݄w)#Y(B!B|B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!Y!B!Y!B!Y!B!8k#/ԄuvvLss3===D\\Ǐg$&&YB!Bq΍l ?c#H FE6:Px<۷oP?eK!B\۷m۶ /f֬YL쳿F]Zb!Ba8g+(U %++xmgwY~Sxv26e=??/q|}Ύm6 }ƪ/<ť$H}:|xvodrq| ##|^,ӸL@,)tҴ?{Lht줩L8]$MJe8xKbtBǁ#q֏ۑ#Lb8=;6?C#?#F@OϧicN[c#7{ >B!bzl3)-dI:I=l~~ڛ o-Lmbc*ֹhg W8yzw~%;o/c5`-%y݇URq;ְ1>0s7kAK&ݞB)CsPYdF\|)߈FW.QȎd,5@ljE;)OBk)E, c7=}@?~>{>^L2'F3 b4G |~gWpor><#=X4%cϡ]Z Lӥ[xP  !B!LG&O4r퇁õk]&@˘+8ݍP;]sS&]s+ӧ“SlL{O${tl'\9Qm=t; #mieDW/p̽lzy+QLʖq%ǚ̆7L۹eڗK;;;4o1l#m7a;l}Zba>m(&nߙ=es'V1GVvo2J^j@W3=F(gź& Eڷ'v;5徫Z6SZv0g,Fa] +7}|R-wװ{NR(jn7VRns>-f@P [؊el, w[۠[-$~I{OyXxR8;#ܫaͻ:>Ǿ}5SwOraP+cc4۶c_&E=c+@>i»c>m<8?PGsL iw0](G |܎`z\1# fDu+޿irB!8#3e\;{)ǀGN` t]y9? }^vGcܽOodS¥#0f)@]IS0ޭOɦ,S`M+ ʍcyk/ǀ=yy d![w$H_gnzbS!{$H޿K*| 41z;6%F\ﷃn$Ɔn`[m`AG \ՃtΕop:m|dKcAbBבNKCtw8 &=|z>W$v!Z-|fsG~c =>P`d _  4uj>3GeVY!$aJQ\yLV'0e|8HH„8w Ij3ÆWwQ$3$3Ig $1IJXI[[% oOس[Z{V= mE\CKSQ& a&!3I2L& W?z.ys3i#Xú=^CQ0߉Zch=\EId:ŏ`0#c@B%@O[Hm.( >q2OԌ#Yd$T1vb]ؿSgcG&}N]+!Z*mɸ{|xjwSv0gVi:-| E[35`%/<6XlLf0#bb1E.y I#F[^|]vAQ. ,A :6'<*!LhÅ\ÏL FO_vAq v\4Q~խutz飽\GFGP0ށpymo> vmmA0"}vޚ DBɎgl3SAd3޳ǝΕ,?`^>퀮|`7>l(b,9'1e(VOd5Oc!iϟ6b~h?rgL⻁~Qx3\ cMѦh>;: \uZ%>> -MQ2\W`! VB7eL0{=#CTVVj3pXOS6oŜ%SIg>-ϱ=ν`JvSqG(^;fV+ d / sLP{ްnRUO<Ɓ$}LT)JuU}G{#LN,Xc#a;;քjS& ~l#矏+ݙ xi>tZ[5 =r)|~9(%/s(t{׳l"i?aeFF^2n ^o۹^_'Xb-8\)2o;&AfRN~H韚ax>pyxF gocN_x]{|f ֞zz{,~yO7Y*33`(c8@>4k<ќ1O3M[KG:ngĈ$:48s_^/`NbXr6ǟ9͉C0_Rkc=$ϩԡ_4n:^0 IDAT;D;8ʑd%'{|xƃ9###ǐS̓lzY8bGkm09#`vg8)yWRbʆ-g`2?0i&-?/31&·`1f Aoxs@+{;/} 1/ߥ~w;/oW2]/eS}%嵝]2eb ֱ܃XfW3h?,KUQZYIof!LI;ZG9dLg%7楗)9ȹdݎc<|Zr3g;܄ozֿSIΗ) bKN v#6\`p&aKw1琝U4hܻUf](yʊ"6n>\J&.}O"jvpt&mp>-cݿ}~H$3r\QE_k:K݀˒O>fߟp:n Fho9ߚAk_#&`uvqӃ7xÒj>iZ5p)~E]x ch1ۆ 11&SaYDDD^H?~7cq$&^klbs9)s{eN|̮eT !0d\e-9_v#甆ug." DӲI3`)I?2+CLjR#ʀu$ 7.W-K H=6Ǧ_ 07*0}\BBGe>""""_S6\N%& |K477_z ]NrSY{+(ԹY:Sp`IA-RRRz_xh%%;5 2'OgȐ!:EDDDy}MWA{Ldo}SزeҰٳgcXtuXv%Y]]]gq\Fjj*:DDDD _)#FСC׵#F(,&%%B)::ÇE}d2]L&YYb-""""""_lX0au+o„ ]~` Gh0a•KDDDDDDnJ1b&`0xUǚL&enʤ0bĈ <5G.9/r;MuT☆]ynM{]Ǫ߹.X{zB'kɥPM_m^i=]"""""rCn ]ӜE}}= tvvzFRSSIKKze!y,~Q om{CW~[s5ę|_RQ`Xp۸۾܊t)Z;mx|OeSpOfxJ7Q\d2|3ul8e?M#xm)s{솸apM;?oc`ׇ?mdd.2^=|wT}o.〿cHΞAzE]<_Jư&?v2e]ͮbK0H#;pǴϰ}o8ƃh0ٽD| fջ DN+w{.^-*/Y<-wi2b˜Y9$BTvh?3 wER^Zͬ[g@TKqu8nnѣho%X @ }Mw2wA|4&y MGlDسǁ#^e&oI!Mzh?-0(h0+X|]lOc )\0Ζ]U lO̰9;dw4 W,c~F ۶wOB x-? Xh1ؿc{< {{2qLJG~W@ ?2GY$7SiW?.[RTLO?GNeh+xm%2ci!I26ǰ'zes3wbHc#I $`يBzWZ8no_cpR{}²Kv#Ss8PzZn' =d3s)\G0$bS Gx ؞5ȧ?)%q ,EX1O ;3J{GhT}`Lgxt P8=ِr;`']c$c|VlO] M#V22/>wsCbDf Jn7owf|%?nr0f={eR<Wxkk:niSDDDD䋢)fz]Nb>C #`6DC\1=.AL1;bnj[B`/N#@|> ̘#!=@ȳ-;_D+Q&3Qs f^$ίy2c 䁆l{` zp̨}h0]*}P2tz#1^xG^)v4UGN R_9 )=kUԣ ϛ^<ijةXDDDDD 8}Bx7Zasl6cC"jmۨ-Tgx3!ܿ[M@1 qf#gb^֕P;{ pa˙1phBA=, B,dX;t7k넮&2e ݄Jk0_?/8cb$߳+_}\i=.G{/]aXN6S]k`He/deeZA S^Lũ r\1n/p=18 ؇$S1v <}@k@@P1CZbH'MT hh[%0ul a%cc$:_1M4{ > \wsd=~Rd?t\&_?CP0Gx?KS0|\rE ץKGTY.8tl/ /Xj~$!{SɈ%<8*4S]o;$Oø a2Sesk( &ct:{3qػekⰦd3u6oLqBpլ2ő<: P709dr`1VnY!q ^ٶT쇊x֐13gExm/@;'xոvf1(rf5jд#6a236e*bqM'1t|:5%3\VىpHnH/GסY?}gIL"""""_~c< b"[3g޹{Mh!l׳5;_Gܦ#x~3Y:5|hU=CYDDDDҁ_v#GB@a3l炱\S{YjC@w4B}BcTXF|SUT`8h ԝ pj/2'O3"""""7dȭx5%[DDDDDDS`Q`Q`Q`Q`Q`Q`Q`Q`Q`Q`Q`Q`Q`fffffpzVx+ ]2-uGrIܝSk Dߜ ftU(mMgD1?^]3~)WE~cq4a1dSd)-p~g dC*q|6 <>ϽLI2_̳cAUas)5G|t>1Ǿ?]NTR~t T\㧳`~}DDDDDD人neʜ>j5y/SkÆ-r#tpӿ5AFꕗ)tJ4y(4p%@J6u5wE9j%^\7i6w\?0I~wGDDDDDD;MU$RG $M'~X&m?SNî(3ßx&98TSGД1SSM]t&{; "=[:_H XxNPSPP> 2(ER44>D[Z du'u4v%{mtb6FFJ`_KM7KJ%ynLfD]mۀSu8 `#-'x}F٠cDRV +bͺ"J$s< e1H簈aj)W{tIpzomS.2GXpe@W#A D e"I)!E5`]ntsG9wbl-V*)yc=`Iq1!/GV.gPDZ:EDDDDD_sW#E?]JI4>|"q8)ǗNĔyk ;WpY[M&va.|4Cǐ̀厬ɴqVL@ Ya´LnJߎg=TIy;ϒ?A3{~rj'FF{dž3h %2[m*6Hs.x"֭ VK gʞf:\KV۳]F]K'#sy'7jt6Ǔ?es&3;Sx?ah)b+;A\*-dRL5/s6]DV[Cļ~ɬŹ4n.椑SvdQS|۳tk36cOS>z Melxcz˿o 9j'9LwܬElj" XΙMGe8<A͔tvCܰ?8(7E&&ۀLNw{xsCr u`Y#ea6Ydž#N>LC$\*ODpIΙ qt#ogkn$YS.x/v how㾔&oʶ';Ʊ󛷭y<~_ ~EɁٰ>;4fV%OO,S1:] Y>\yLM:먮 ‰RV=*&'yc's 4ZlXZ8|ZGC 3Ɂ(4eGe"~lJ8`4B X0цU>7odO-)dVN#nw `@;c,#P.է1燅xg.ʪ#Mԇ&p.Sz.Is>+ )\:x}Wz n1X  IDAT(sgnaY^ڴO{<>'O,nۭ_~ ȸp|NY8F+ِ6]Ʋ緧^BELX8TzA 'kd|=Ajv1}rg^jf1R,dY,r? U" uuKnb#5x=h6uo-`J waj Cm)RX54Yrr_IN\KfN\?ﰈܤLqXCGxv|NI.#IK1?y5苏Z1O ;3Ȱ^N)[ V: VP> d~QvNdM~780}|;zL(-)UyAza];'3Qs f^zzti8W66`'N'eZǾ_pAg֭-c>%lՀmpƚ,4XBPYXӇ`+tQRs&l'b/ǃn_+cq9ĞĆ+#y/,;\ އ+FwL9u3掃n?˦q|(sPeN̄pn5eW8)yWRbʆ-gпlNnaRTsY/+WApڨ~F_3Uװ2uHUGɫTVqe%ds1Eo'fk Ƿ ȵ},mTm^Gie%)bOcǎ˙Dj4grʣF\W0<ʄ1?^cO:DDDDt6U$aPtt@k@@zc'oeRZOTJ6abc1oZ'Lp`ݿB,@j  eRqŁuWT1NL杝𪝟=zr Rd\f^]]Ag&++pQլ掛-XS5%Sưyfwa}x'oȪ@T gP0:T=@='@swH_O'!z3dn&""""""v`mç?~JhD`(>ܲSOߡi cђEg' +ȧ_DDDDDDv0p@t8oIXDŽF/t +Tz9ݟ{sHP-#-]$a9)Q }i>ٵOCDԕ&_ۡP'NC||p:Oq=W3*w>OcIHqq%\eS^h?TG}pL79[oEGG#G$//+ydo9KeMwV8QfCn'[3I2}w'Sn7fࢅLݜMO""""" 7`߳çhlJJ=:bzsH2LeKtq*䁹sa 4 0ODB{ȏ߲$/ì靖(0:۰Q̜᳏w>_Ge].1`?h>=ȑ>sO&Z0Xq8@\5?"vk!e<,9rH4޸I1xC &S>o&{LN5CٖTS)߉E`!38CR]Ḩ4Gv|q}NcgX񾷎 Gd}?4܇ IRLUc sJ ?Φ710q/Mg+H^IgTc]Cch d%+)z O ;qZ#=l*3yOټ7h vBT"c0e`ES1T2C=>%"""""vk%h!6:xXF߸m 8â=sccc/~l2eK pj0طDf,-d2)Tw["x~6NG3?+@ D"jm3Xb9˗.b[uɞ5AWڲp q SyG)A6$e#.f6>. VOa-&X!;7QecᏖ|I1nܔp8 ?<|i!y#,\9v-!Taǝb2_4Q¹d[x&.pE!K1Sn~;Yb9˾7P} H.fC"I=\<:%\o}hdر}z)a鈌:51Aۍ7rlt vQ8FZ'X ّ2Mid9?Iv:I]^ESKVl3\A5ȋf#í1 %r;tq{d `v2k .Ϟ5kRആ7mTzZB440;$k`ϣp>$w4۽C4|DbzH_"S%..of}Tm=G&0iUUORr-\ihhl63f̘>a0ϟ1fb#uDž@$tG@Ϟ@ .tk588;^wF>YK"R{fϙD" l{` zs SS {wv/VlB!CG|y3kXnQF3`@w(3shڷ= JKw#ffé˴KDDDDDKN]kaW99q#GpQV+ٜ>}O>̈#0{Z5 l8@L{p8+LS@;.h`5_uӯt׳YȲvnb׹QVbb2_رb^n=.2PϾ#@Ɍ9ʀQ^+>=YAqi Y.f@*Xb=`&7ᾸTDDDDDQֽ̤ߌI#!6 :6c;[RAyy9[n>78_'{ٶ$1~Om'8 8F9~`8$|PLW17* G'}p x1*<"v״8@ }@x8[nkQ8G0Rn/;h4w{=EU5qf?O~ g8H4$؊.8p}59@`%1|XKK(DGDZAowSn|DDDDDn]$;K]g]LX~7?ttt2k֬ǑMlYDcIl cg2ϭ0p 8P:<:pl3 ]O-`!Ɏklt;'xոvf1(rf5} ϡiK1/=A'F#slCyy}jVHZ;Gq]%^2p}nٜoK<tf~׉ȹ27ֱN#4hXĴW5$<&)lSgoUJ3TDDDDDE!sȟё`mL@ &Ş9s杫~OKxg/Sy"lF[0xxJr5~ځvtg.؀/c_n9,ǙiDc*ŵETYE| n|`-a /$?yĠ;Q8?ȹ;#@W5/,~Tf|^h&W2!aS 5[oN{3\Q PٕʬWս'ltw!"?gQb*Nܥ,K%\6f>e#\6)cE c4浗)oM14 Yϓ?}}X]2хD5ORGj*f2z Ɣ3R08@KO6\Z;k%TJb>مqE/ %"""""r#^7A$E_Am4F՝4!i?Dz0mְrvF٬0=jk؉kJϳN.}͔0/#:a'a'0bF7#e 5Rc5b|5>55mP cC5=/o#cpt`x`"tl:0l y \\^^kZ{ݍge~ޘh&L"B!BI%埏JcSHX')F1_Qs<[6\ }+_JB!BIg1Ӛa+X͹:??ݍ4jgtE=HJ#Jψ3Gh$vt8 s0>{:cE&kVӞә58IB!Bq900{_ g^,}(6ڏ)1Y5nb3z'܌w6>>d6lm[K~u ^ZFϚOuRi~;B3ңIYn3;15:I.B!BI3y1lxM6|H?F2#> v3f1I\e#y|w%3:f,i?l{{B3_yA7 0~+ů<)>{S/lNC#;wB!J1(7Djk%Cbye"04P8)~)f!B!u;hP&_@'{Dc@wljX!B!}H,B!BCd !B!2^-B!B{0 !B!0 !B!0 !B!0 !B!Ґ⎎:::D"1 @bb"ҮUݾB!0wwwiRD"4M UU4h+qUo!Bqq\)ٺKR#N]} !B$aZ_Z{{+qյo!Bq$rm8Gڕ۷B!Jnuqۏ+q%o!Bq$B!B!$_Fʒ,|ы{(Y텈W2C|疲g?>.e;>smkXTVC|Ϛشb)U;Np>4}7ʭ\ե,{;~.r#<#̝;s Z.V MYHh.Y\I9[uX%4u:qk}_P|){4B!$_X#7ڷsw=x^Aa3+XbeUT8G#'rs|PMCAz5E]X.Vn%_cʕ\~_n |9UOOsuhQb+VTPz U4Ufcy.·iů#B!.C. ֱ?u) 4S4i  XncRbǨ3)\hCO.C?؀:|$q.$n姡5tϧUP<% *g0-byA*[ )ÊJ:57r(|0{vr~THĨ·ꫪM6{"<:K 1I^|VKe d#`mGZ;B:6r#:B5j0Ԍ3'Ib٫:OPԯfS oT.&ed?lG9/{uMp.\"@܇ qb|ru7X@M{Jp!BK͢hn6e$6clՋiJ uxlSWS1d'):r<`l1n=}RYK)bްsx'FIJ=&& "Z(,ƒԧ b#XK 6P\-ʼnW2oy_~I^ w8U˛)ʳcD'כ^Bl rR RJ<7 3b&{"0;F!B\. x8 6[=g1 h>ٌJFGTF{m*YK5x4@p*iMwbSu|;}VMvWPQZh[q2S-%w@fܲrʟ`hgiD1T `r3TEW70?ZAٜY8&٭`;ٌvQEiyy)9~ XҦ4pdNJ(2`§ȷv0ݜM FӬx 1b<G(SHmSʠ͖h2_L ɊFStq~cI~?qFTzw1OuFUSm|;xN+ Lȧ"[T? bG+ ,GtT9Y3pJp9lX.2RL-'5^@HD3!1-3 _zE1EOLċ}#ѸLBs(b_\I͇At@%0.g4'&XGl\Şa%MzkU81NK%EEi |̈ ,hGFh'X_3~Nw"~|*mYIV78q1q9ܸϲ(~=vb_B!,\:SuABv<YŎ  3r>7/2h\OZU$*fc%^?؎yZ{5MGU{%I44M'2J6/>܄fSN;5T܂|擓v}IQUhj4@#fa4MŔثUbcw]?F;ƻ:Qn ,whP1syZJRM؅ԲjE%<kB7ZLbKvP}L:x%GJrʩ*M mo0uTTIM4 EO4fl,8فMv'&&a1@981ď2?U=cC>Su­:ǷDCT̉=x?,zMd͟1^A#ܡbݼUhh(=oi erF鷳o0܈5xy]?/B! %/q5R̞+8jXG7sN=k{߮cLrni`Qc&#p'޴`߅qLVá0fά Za%H8ф`\8.GXŪWgjF5ءvAo-fT=v oD{_AЈNa41qTCK4bDU5Z$9rMlt~,{iFY.!(AEhi IY8qhaX@Zm8:ڨϥtkMdTN0$ƋA-~܇Thb25 l:aLQQ (<8GǴ'#%`K\FewOP4:*ʰ(xTQ uaND] 0bHQDEVϮޅB!eŔa?9Uq±cd=e v쪏ذkƹМ@S t+GTWTRd!wX ZDP Ff#ʱS4WXj``&C(]Ok)OۯFF#&~0)}hzM@{P&ٱ$Xix6GMuT>S/G1Z@n&\o i:uW"FXS15i6aDGSL-h8kLPpN7(qc?1K]U5;C'zXFTKz\k=Ìsc˟` oϫ&(p9szcx=k&7.OۯXVQ {wk6$;nz4Qu .LCOyfwOHx\Jn^YDt$ՔCX>g> E.8^t,C?+*rcie%ؔpO.gkB!K&eEt]G>dfĽsL:Yo0ϜEu\WD}AM2![!r! %4lWSʬ\*c!t;er%I+B!\Ȕlqّ)jkB!K&۔lAv#!CHW]B!O*.$iWkB!*IE!!!Aj^Ev%-B!.2/99D:::unנAPNWv%-B!v%B!TJ !B!0 !B!0 !B!0 !B!0 !B!0 !B!0 !B!0 !B!0 !B!0 !B!0 !B!0 !B!0 !B!0 !B!Bf!B!Bf!B!RO.~zoyyy<^<Ě瑗7_}U~[@^^E7x3RDy͍T#/gm%yyy䕬吴?!B!?;ֲvwYXM~qox'JF)n׈y5vff3:)tAZ9.`(XT$KB!BK֐f6,F}%}6>Kq~qgwqh4uw[O~Q, O!B!$a{uNxJֶ+#`ĵit#d䧬i?!~\ T=³T ~Oȫ[H'YߜjwF_<5/eFM{13 bvld0>LK䟎D EK qۊٱ\װ-4m9{Y߁5t-g3VƽxsS1$N ^|gdFMgz7ex!B!8CoJvdLzy<_1¿`֝pW[̓+<|[XYᎮF,FՎc:`s+x+;аſ_8Ϭ[H8f>/B!g0G:I>n[7Y]u#v+տ?Ŭ;gsxziPtا{1asho8}FYk[kpZ|/?Γfۘ=y&m5Oq2~,Ih@ˏW#Cs6ncH373| k}L'8>@'3V'nfKzf;g4|~ul$O|̶bFڀ2?/)o +m WN !B!%0$'/ߺq&L?nF`jM9 'iI[p7I'9w?n`8qm9R=]-ObsRg=4 Lx2@sn?0F31'/mL?D km3p`b&oӹoJv!B!lf v )tۿ(ү @./bz}1<#@2Ŵ4҆ѢOOclo- - ͶVGq?mo?dxS?y:' YsB!Bq!faVtDNN^g;^ϡw+xyix{i#sczj4q1,;+xӘY0KoMg`_W`w>~ #yLF56<5+pMcGyBй5<}׳q|_3F<;!a 3yQz־LFeFh4}Ng-B! T6$qy <<dfz*{7a~:s0k<7>v#wh;h;6fLJ#d &a*{цAd&7?lf\B?z ߆p+$p]4;'yY8I lJ=MIq>Exk&1GOQrB!BAGᅬ'҃bDD`hvww7H5 !B!A2 " z#BiRecԕ/^b3nq6/#?xM'w<>{+sq.'f_Uz!N v)yn*gs}ܐfqE:i {nk^`%_cʕ\~_n |Y\rV52VJ*RUb#~\  {hWWމ(_Ϧߣ^d!Nͪg6uBhC Z^CߖS<ӈ帼_~=MӟtR ӟ2M3dO.v_dvz'|̓{ݎ Uޫ.nz?+V(]|*S<}fE%p|Rz|flB\T0s?Ύ ]ǀLn!t}7 !&͌)zvn[<|x&uб|sJ Na׿5I{uMp.\"@܇ qyA*[ )gܬM=oӣ7r(|0] <[BhYƢh֭pFo%@/Edl'0mP"!?@뻺 ͧ?WZɼXD{,|w*ËfR8babG,3(ӊ ~ڝa@GCCYX﨡jP>،# #h~WWIErK8{u'S~#w (@`Ds=(jZIPy(Hm?, )˳ G}ԽPͦQg^Ji__zFmT> Qn*d=6T4|oµ%2܂#ӄwB|ďIN f+qNP c[[3摩XGD[=B-cyL|xfQpF>ۋ)Ll^5 a=( +;KY?1󷅆%9{ w}L6?|E/qL +8o7B4_BOOWIc|2%La&6r`𵂒h9\WXNծ ሉɟl?̾eo;0xZlSRm |J~(3Fda-AJJY=I~IvC8VA3QouQdg^ye([>pG16mk5]f WPduQn`'J QG:[VΒEZO'q],8oa2@j\l=UNyyjUoH9X:((}oWIg3Td[CC3м a2TPRg_XǛ?#)VB|; 珖P8E_yKY0ޫfS5TRt9Ovbyp ?jG%*6%PL9Oclv_2 c8%13qU!z;J)%9q0z N,de{Ф| 06/z`/x׺p.iϸ>}LcھfRRʞFz-a)at}ʞ6Ԍf3uLȧ"r)M>LΊ wOMؿKR̘Ä[!OxBO*f2odǥ{_dSxG|c؇8? Y#9 seRldEKNߥ*n sPHpD8O'ٱݔv){& X' 6cυNBouę-*цsJxk8#Ϩ]XA趨[C8H!Ĺsq~FȺ瞧 `TojJ{^VVltWcv$ %)2 gnf:7]9SeJU;l6sTUt:[]|&M:=ZQ O, +5 4UIR AQC1cl!;(pM魀r*Ktj;ERQUp86:ӪMu5o  :G$zjzopomc0x衇ضm͌9iӦa0j904x;z8~ h<1whRj-BL?.~PYy䦇ю,=(797V Mm{csg3W$^_i*&ڵ oqƝQ5PU ~Gw=ʼnq-J$S%Cc= /i+P_x鿼_. q;5`%3Jfp4gc5B}7w=$h[C{xhhnW\g5O~Tb!s,m8 Zp)iՃɒ^AڪER='5IC83:5Լh#XH!e0Q1L;‘.ڏY)ӲL{±Vl#i㿗BRʍӻ`Su}3=_jț[;p_隚8x F|;{nl' 4'e0.»OyWOcI}k%Eߧ Z$Eo N )s)ZbmY#e05 zFJY Ffe K\,{ԯcAƔZbn֫YFFY*z9t-DÈB]w6p8֭[1Ll3LC0O*S|gZz~*%QZ ->{WP{F6 ru ,TG;v2M4RC̓ZGx{ 7(a^ CmUk=9F/ B۷o}n>>W+<eFG:ꡡN8>:{q-vb?KU7 !`a_8ƻgDc=zLO6 /z`t ]Ŭ_b{ڰE ڰjS0 h5w_^o7S/Y=3Ҽ kAOmV Tϲ'A.>Rqn]:6{6nKK'ʁ-ooDW_ piC肯Mdb<;6und_.\ʵ5&r(UElm cIj\7jŸs${-5:ɽUa BhA/uXE}Q=nƳ͇2>Oq V@;b_iTB;~`drMCPneTm:Wfݺul޼_~?l޼uֱzjt]x9a? + ;ȝa|A>"!< tȃ'hf (3*puwfGɞXkBT3;s9PEͪz?ͺ`H!Ĺ7(7DG5!@ =Fq_6lp+BiiT%k`͚5|']ĉ3gTÇ /r~a__5 _52oQfZoZ4haP8 [^&8˄wРL@=:N"Dq"1wرs>qD"}絯lbyr^qa$n[ !L[K`OO{ 2roEQ<;~S!qe馛hl>Oㆩ7 ?L d;Jd+^!Jd@Suld !Y&2%[\M7vO_H܄V!.ĥ@fqYwTqyJHH8K%7!g!$B5tP"$%%I%؊’~ˎ$$$сtwwK\& ($&&2d_/q[%!Y!38 BW2%[!B!Y!B!Y!B!Y!B!Y!B!8VAGGHD*C\03=RiBqqڕBH,ZhFggT"Յ 4HڨO%Ү|)Ⲣ`.iBH?PBIUC*A\ۥ !D\BJ!$aW=M\mQڨO !$aJt[6*!BI{U7xFaYBRerM*tMdžI[jm} t7 *QjP2~lX6@EhF' Fa9}_ZxΏ\ι9B!B!Z 1uyʙϒ%K2+ Mίbs9m?kC/:.f.~|m4ڀ;pA Dpy B|x'^ZcM'e-%H|~1ȶ8i6PTo=SB</*p?mO~=tZ/-ݏMU#OZO|yWO?eΝ?t6)[ X,Ff=]43bF;Wݸz Xׯuc*~@IbF V/2`L[͋WzJMa}ew+e9< EV?5%+/ʢB*뷓7YbP~@UEʔﺨ^wMQQ~)UUUꫜKcCs7Y~&{E^;](in2Mee%7x# (ZVnyS,]ɌqV?*LƼd;7,.:JK9QIANzP=Ft/sV ̸7 s8 ٬@ ssi7Xy2RjkmLiiDu/chQBTe&S0ٰ'MV\m Ji-, _!&GIg̘nlUtaq":Bű;6TdL"VaQzJg4l[Ƴ83X|[2Ƀpݠh0Hk;q5Zp($T0`=Yt=Cl'̊Sma]5PAwAQm!3Zq46'>7(\eyd[qaH0Rwb!lRR(L35X}xnȡG]Sygy;1)H^ͺaqNiOA ;-8(&f:ޮ,${*jtR;%pwS1P7JqW|G{TVVvKK | W"::#sNJO XH_2 m2VIeSQLθpaGGf[ )*v,z.8L}ycCW@ºq fڂ}bG뾯-\Tp~{Q cf&t\vUf3&_adŘZ7"׍ip5%X xImŊ}e;UTf*݄Q_t07Y)0-dk ڸքy6lw/ @s ?70rdY 'ȟJ!?o>l:s'chY rEH>gJ%[QfP%^aFijE7ʀlSYIiD琷 4<ſòBDZ%f0%g? L"*Yņ vWkd^ V,RMa)ĢL231E_Ԇ2+ &_`ٰǨyd>*TW }'2$#A+%0 cZ`s(=jj%rQm-$-3g ihw $YJ)81cLj61&LN?0$L8PmFd& yX;Nh`4Lj5+20kFذx忧!!!gթ^XJ *\6Uw]K|rST+F_ޏ]0'D Zj'8afLX>}=*/fV X, cqra;զ`c:ΌTY+a+$kY<7kғļXP2No0MӟGx_ O/7@4Gs]>@%`PgN}"O+ѯ=i䃜,{f˞YƊ Aa$\8MA~AhOjʂbF0ҏe9Y)1tMx$$$`0O~qq$(0k,FMJJ%TF1FWVSR~qF IDAT!Frj*]5|V`}k57 } glp Q3o"̊( Ѣ8Xq0ݞ8@nwoyTuM3&Wc+zX3QV*Z 1l[гv ט|-A1 r}leLbhew,ΙN CM]zQQ8V+̝;;C  cGS;,Cs +9TƧ~CRQ%a'yl ;ƨkLuv˥‘j0pufbz:˜tF_˫1\g|b.@eqݱw}eg` 1\4|*Oa HKqM N7d=Nhc05J&/ݦ|z*dGǸQ p 6lzČ6ݳvuUTr\{֍6嬤Jd WbS,Fދ;1ܑArrt3u-0( 0sLFͬYj]Rdd$?O馛09V҄I;u]._s&Ə(ӽq1Gֿt̸|=aRjam".0e #,čD;rx_3m0%Ee<;>0Έ1Au* !ķCyL52=$Eo'og#adʹVaՊD>Pw+4{ !fNzu״ <ϸ٭l=MSk_Jk+l ߑ ML &w_?2$b9x 'Nh=66S'+jJuu5mmmTVVrwzp[ >+ p҆{iZ]R:mg֌qj n0#vhKPNru* ~QH, _T1BI݆dUtv/wۍZ*;@tL6[l5ftX^xU#iCuIQ_Zɼ}[:^FNR(8cݿ}qx[6ekSCCpGG]PJ_K-nԾmO~:}ȃz''.AZ-S`w_';YɂjT1y ) ڳzHm:ghG4f6$م;D^nsl/W.'\V/Б5.4bW`F= sj xrN|vMFש +ZtA*.n-a}; ~7s0Dh\P]pϞBcT/8FLxt)ϊҩv9s},/yA,]GF )7C!~Psmh: ~c yy5S5^#o !(=0#:%:-*l? !m~rԇV铭A!m3'ӜS[ծ:2r1cvTN7SSivּBJNT?P=a ⺛8 ٿNG.^0!|29Q Jp P3 |Qrsttk 𥱹c!C:5UOt-_`ͷn&o]#;;zw}^zn?c9Z_c%U,{.Y5Sn#/kիPؕ0t1cvuWSM1 TPZ}  !an;;ءUEE!&>[6Vb]2c۳<ڷ~mJ>帿EP6bւ?eZ %N_}h;9VPØ`zW!6&\ttZ@aeVt7 ϑ_DPP}C_^F.(/ 8fmFك.\!ҷqhO+4 ^TPUS(20 ÕSHY4xF`)w味<7"k7Ooc14݌,UvW]F;Lz-ZZrOUU#܍=PVUׯ=._:Xgy~g4.|݇6oAwȊ *,{*q)VZ/,od?`fl};1 a Zhs.HTmI 8¢0ƺuT/~ nNeFNr޴/g@\.퀋 {f=z4˻oMvv6vbݺus7by;|`hvHT`yrֻH/`/r܈iX1@ [r[yw>p7TS6nQ=^ˢt:qՎӹۀmbuDH#Gd10>5ΚL"a":F5hnDH@nz6m^ÊwTP4n3(vb{ۍ><72k! [[Fĵ6a(}"/ !,&GH6Ž8qZ5S34 ATwVl1ǓIbȐ!FN:EDD۶muSN2vXv{3!۠B8SoYp H (XfІ`}ex{:/þ>( '(p\ix<'m }A2ynLH#=NƮGB7(hƑ@!*ʀnGň+$aSLW>6PFt{z{K0F:ͩ$?mG70qb݈JEipR]=Qpұg$YY{:{ v;p}DPl9sJ}È!a@WhL`JM6벶3ˆSFҾg)cSI!^`Y\/' 21vY/=¾:F1l=ՎJcB e.TEGT8L=C`l*i9d?V{J$2Ŝ{cUøg0V/6 yn&{m1`o}o(a&fݓAγ Y0)F2 C {#ąÒI.% )0^gP * q3rO>B3Ntu!/_}Y0BqO2 )_?QwfK|w r(Jgw^v'ph4ػwoʸ{1b}eȐ!=O˱S4 LF FZ/׮;DZr Jl!-GS H(Fqm\pݺ $nمj%$kƑp2qzL{-@B2Ilˣ0@ھf8QI 6wo<_/~%~npd _s?5&&&Tqf-M#wߍecXF->kz<뚇~x_[c/-R_Q]]}i~ n+Y w#FΟ 1,ZDVʕB^v8 N|:oXEO{`,ƙh4?AAA>FtcHbʰJ w~'&G뤌P0%'޵7ō ']w0w6bz+C aРA_6:(\W]P= y˅B`,'}eΜ9̡$~^Ou^:(\k%fߧ-eTdh2ic%ǰ2 ]G;į0fƌ-oۓ~InBbHMyB!Cwʅ U !DꉸJ!z xeH! ЧO)BH=PB;_~ $eT'*WB!SF#!UEQ !D\BJ!2E|ӷo_N:|)kwe( }=p@)B+!Y|/ISHBBu!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!Y!B!Y!B!Y!B!Y!B!Y!B!|֍[ر[Lb1<[}>ʊda=ǁD؄iLzB!BF v4CL Zy//,5&~zn 4#'k_(j(~#Lhh_z5G #:}{!B!ķ0oe g &h& [)Ze6 juԞP9tGۈuS.달~F`NX_[jؑK Ӥ4o4M7%yBAvhv(hwMʣa}9S==ԗ+iRBs/I9:_esIM-JD5.!B!w7>$!svP5g<8' # <φmo+_*&b{Q+X&b"Omul|m6oL ܽ◟`<=SФD`2&a?£,m+uxA H̷_7 -!]L5Ly3I[D*=ŹOcaILM63W2Y cx}4M$[2adh;X1+_hLQl9Orgxox=ν[Xt \B!BoK`xai&n'ƶe [2h*=l"GH}5ٯx#[i$Sk6l,4D)14MX&5v+NOM'-h lcJMgB(5<{hu[S/ۊ[}þvۨjF ^Il(vD&VQOP,~):ڪ-X@w/`fKc$<'q{Z}zhily`8'~dYY{@Z^EͤF41'ԈYhd+ŴE3il#i<Ϛ6Ib~"m}3?r4PKcMq{[h9~&f/^MJcU;m"wɘl!B!w7摓4B8ٺz%[+τԽŊi9R 0 6i Әh[[6G}ЊտJOE.79!U& \W=չ俔EG;=<*BûޯmF=y7T<*MĎ l;{jk64oe+9lݯ0x[Әh6@I#t{ď339sl+GOPf>9kKiGb$RZ_?4vj _Kj0=5΄&0X_[R eBK=e%Mt`]B!B@N_O@O?V|,Yrϗ^kaܐx-9qW MLP#7_ ֕n*} IDATf }`;00vz"W(\Tt64\=6͟ᨭT] 7Μgpw`v328zgA7ݯ U}Otǜ8jD]=[~3ߌ #"Gi<~F% wN9l`522k cGc4(*p4=c kh'a-;H#ʫ ǘH 00F%`}-Dސ @0 ߇hs^<̤%٤w}<XswDBK\"*V8# TV|WdlpNPVG06Bʑp\ŵn!Lo>)#t{sX6lM>b"a(Y+YYMQUy\2&FZ7͔׫(dmsghP-,ԜС LLqv޴P~%+j1.sEsp,\&DIBﲿ78O8(:X?/sH /m }qgpr*r/&[Қ}[XR1-}gl[RH wRr'd?egM%>/dk=?/ Od/f!wzz|m9\37Sn+^l-+{<ԼiR$qЮ{(~e p ƒrLHBoQ"8g4+%ljUBICͶ5c*ʠX️q}͍d#*ʐx&ݓQb#L"mЄ 6A=-5h xToK#GYVT=^o|H&f3!DGٲ_ymRپկXiHiV|HT4+>Ly<?Aֲ5NJV XlK,Vяݾi,VkH|ŒS/il>2IznUSlwZ(ūWD<.H#ݭ2vS<>3C/'<ަl4%J_SƧM%[GKOu<Ń}kߪZ(~i[Bf~?p/l) > xUTQwX1~ %*8+h"JGgHOfJ/ҘXFβ8΄?ӈ7=౒5',h,POx;q 맠(\tJ_C-Oazm ˲4gFdP2D,͐hƒ@ ]8{=AFa3#i,%k*v gIsv ބ8v&@4^ 8Kv\Z-Hc?nl !PZDJcS#K,ߦ_}΀Ww?HSz4<&'-^ գ sJѾF"o)L=J֖\v$=„`A<~xB$CCF̙/3LmNj)~d:Y= e9sY#oJiZMkٰ{   h{ٽ$(7]@8l|M8<]6D!DWtNmhoe2 C\@V!܁zk 0h!. Gٺ$G_dGY cc㯏X'0#gFt%V*P RiS$]bkꞵk?~>\~|ϕǬV;FYqai'1YjhGupn!agݶ=-زMel<ȑm m6QZ_W*-c&5sB)e/L |įٮE+3 k~w=w-4I} VtoE[,gj(?!/|e֬Y,ž>}~rOGsaƍMco{;[ ͬa}ugZ̓wN7Ÿ-ZV+;~*l[)nSQUݽ~_oG[qfc *oҹo ,~EѼ<2.eqY^[ֱΚs g +~Ó吘{RB2kR~[BĎb=[(H[Z+ld-LO]7^;Z:o9 ƙb۫xӺ4jl]ubr;C{O5HxK=i)$\Ez |ݖ|K!!W_b̏/Cnv9o93FA7QEοEFs|.e^}m8S$߽Yj1Ig'g\Hz#Ff{අd'yTLb0฽I/N[)L w3g~&7_*9f#}jG9faƼB-nʶRlÄs\R,|8;'E:Կmnv,' E.&әmrg:0\UT8/{M7P07˩`^@@jdjBNHo>g*wR{L!B4s$K'?ށd~[!i: ܻ6RQd8gԍ畔UABjwiT]4SG m(0'hAWJ t~T*fA^J^sa%/p8*ʶ6 >!Y!G+tW1yI'~uQLŶFԂd&2k w?~/%o@M?CBn_Z(Jf&j&,0G>FμNmZ 75*Ǻp '")@tBIN`o:{̚U7p&2}S H'*_;LΣ$GC [~?/.fILieVjh _:y`q qb|R"qR@-LuؙZό;2IB F|js.E)^I8I%مsY};ui=쉞Ab28#QFqڮ` ]w|̦hr ]| nvx)k樠͵g@'{XZNzJcէWLg7vls6EK(Hcn 162N^5O+K\Ur"sŋ)9hfv ~%?>^3l oӾG/mS"|{1}䬹cmOc;, LX\L\F쩦YG5 `̝ΟCPdP~%'[Ē7ML='%=yHN;zXMd=x@qcMpm읿[4;Gq̸v^gP%rus4xO XI 1/׳`=K'-ujḦi8~%%lH2.kY!w_h/Hyl+%enҾΔI#+cvɝG0aɃǰ32 !?u[6 IgVHᮯU|XM~e@0pj(9u0 gX=U{~i| f8kJ(rϢ,p{|4x6n{WG蛰G ?IڻfX0)}lGj9HL#H`ki &ဉDؚ<F:p[7h sĂmlsI c ;08f\3D'U ljݸF<6@}}G l؋vąR'ڂ)'嬨3s };,rjI1"_jWFq PiLN{91dUdY Ӌ<X0;͂)KrJ^!H~!蓸-鰩ucJ&*}ƌ×I dlc8ZD.l^O1'9)=JtTjgjF#ٶ U̙GMlx&0 DjQԂ|,H~{20~!Ptyl߆XSɟ=K]nmd 1 ;Rv>)/SPL^cf0'{+_Ee B&̞dv@ lz}#Ņ9s$qpr@~eZ:]҇$$+Y=I`1yHmT 3>.a[DޕOrXg`$捔"&wőwSa#cFa86J5qgw[h&(ހv}E@"O͔BmʺISVw8:* WFʔջdJA8|@zНzQ]C種1]a[\GGyj233JDu_+ =%T˂.ɨ;d׭yAmC@G[!?#H6|9ϵP`+l,ǩDDDDD.h3Zcc IDATCmT47EaY,"""""gfQhyZwNQ`9Gd `BpGGBoϋ; """""C(0WWWke j ~EDDDDH`0K$+Lp{9 """"r_uJ\uaz _ΰqϋyNQ`9@pwpVhQ`THX8krTTi_hVQ`3;|)s@;# """"" " Bs@C ϽCp`.,+<(0 *8^Ǐ:$eذa5 6t+((0 =wpzB\\ܐזn7^~ZP """""I WrwZv}Ǐ'99yȄe89~x(0 =fGGC*, ͭg|[!YDDDDDYB,f 5>""""",We8>4~""""",WUP/u3ZDDDDDY l_ Ǻ }\CxDDDDDDY|A4PqQ`Q3I&kT6{<5<>HO~@{-kOg}￟]YOYDDDDDYd[HFL-o挣ͻU4q>?Sf=ۿ5_M F3. 02>߇8Dsl*7h(0 UiAMESL:EȸyLL+p3ϿS<\WDDDDDYdȊř?,?đ]w~Ʀо}~""""" "8d^iGEDDDDE.XҎ0+8(0\`Űahiir°aN+iG%RQFv)4;|qNav3jԨ3l."""""c$+!.t󢢢x^imm{aAF{{wذa5 ~Nd+D(0 Խnnq9}{{;n 4,>|k~ClWNQ`9g~z@tt4]w_~BswXNB`Bs'""""" " @v88N\.W;,;>rṿ,(0 8?\h;Hýdb„ :t(lh&LB@^KDDDDDE Éo>;-4woqGzgbfY(ۧtZhNe4=ܲȠC@?"66׿|gal7c9b.""""" "g ]k :[h8q/0+(0Wi}}eԅ 墇|Q`<4({\.(0\|P """""/Z@q#̭aFĜ+Cߕ,)$PcM& 6Lliii0 -=x [k=4? @Ҩ;Hw ^]vzdeeaEDDDKK>6 qqq(0v-9Ϗ/OXsO1W4Atw :}n&-""""ꥥaˆ#"f"3̭ 1+e7x jBo>oфQ/-g ͭ3M#*0A-!@K-vф:O~G} %"""^ZP./l Pĉg}ѣG5DDDDKʅZ+@ XE~/u9"*0GЮI9ҿ1c\爈z]$ˆ ̑[Cݨx9rYsZJDDDDK_")FT`6͘L&-!ЏnMbxl{hp&y;-aÆ1m4M(ҏXfs O}9.nh}wGGGDFś)ew!"""^Zzi\=w{^ZZZHII!...0 N8fvL&#FK䷴vzӊg7Q/^Z`D~SI;~8W& f#99Ǐ]7DDDDDzi {.TFd_hmm=/*lҢ^Z2_ziQ/}_Xų5ϕbMş}]VUE$ ^=w=A+djxy>8HcJṔ7Vj>xG+flE\^,|G~fkYh W{=Bsۥ%~K)XHs+$]4,k,!?*n]KDTϲ>&~o>{s zn>5Y>cZYêk'L>~#"r{鳋:kixYYɺg>_T߰ΙokXș?/hq_Þ >9N.<1Bz|)2l?ZR*yXV')u4BLl2&O|WxYǴ;2TcudG /Zo3Xd^|'OXr97t.)EY6 kܴdϙd -乇p E[k:z_Eb+K3Stf`lD O#pz@4'̴dITE۟G`kX3lbs%w6;Y\]cIIǜ3"^Ycyg0W0q_)[6aM+d IR7Ab63oΥT_þ4|X/9d5~zְh8'SP›/ټR/=0 Y ~_>` iVGۈ7C^5f{M ?beUK8r=B[zyq;͑:oDD"VMbK-q:⠧>Mڧ5,?%j1&NcDh>BCnfʦiWEkNEwܗfyGԜ"JD$ҴxcŜ|ڢm}wg0{$$~, m4Пu׷Ρ gr}LFOcPg^=Hy,#<֋>*Jݺmqwl-%{>Θy^E&}&f~BL<֑ffho;s50ckg|wFp|Z*~ݏp 3Y^)LX=GU~Dsb[ C{ >[?/t\ubLbeXϯ~kcq9m ikqus6,c ֹ u6{""C$nxu0a_+NY?+ )s^lޱ&Pthn Bm47M.dYNkvJ=+x|A:h^aH#MmDƏ? tlkbIm~9e[kF[;Gb <⫔+f(y$w߿7x{S|]A׽Yŀz FvODdP?v4g/d֍д{$‰F@{# H"q ?1"ruXï^%~ ~ p?bC]-M͵YE+`)ߥmd<ϗǙ?jS|4:|JVgmD|"𹽤X e"փ`&+5+*}'H,&+%epRf::ؘXb-x[󈱿W.hώNRAl^OEL* om H6PZUBI7ol_d b "25=ٲ/<3 $Rڟ [xbYgf(oW]]GfsSf9^򨮮&--h:-\/)`dTT@ p ^7@GA![ockbB!?]0?m1&c5mDDDDDg<.T~%lhcLǹ@K}=/ɒ´ Zy2GOde^=#{DD7ՔGy{NOi)ה`ڀ)ޗGh.ş~UxV`m8Sd\|X3/Ӵ!'-5}\efMN/)w$j<|ݖ|K!!W_b̃Mzf|lh20{S evmYmN.T̽qq|Ý{<ﮦrT1s͛Yw8)qm|M&Sl#8ne` X&3?>tf#0#ve DF?搬\T7_dNF|I~i+n½NYNÄ &T̓OQ*mNʶRlÄs\R4E$YoJjoKzlB>j{k?\LyPe'5ZSܟt4a4H?7QyG½$+)< j>RSyTf.?8 p&-]Ln|7g%`e83I8$N4[ [vPlp'%O}E LX-TOO/d*N$%e7iH{/R;mu^h#ɔd8P.r$c3fg0Ӎs)Z)PG۩O)Řul='gz.H?J{ ."K'}Kowo{ۨ>qw߶0;0ewezeGi=pB3f^V/U/SהA;{܆]7yK1K[TnŞࢇe C8 f!!m7d"p$֓`KIشӹi\k^y#y vKo|ν\k ?FL&IF zpP: ]y%"r5ssc>|8Aճ0mP Fu}Z Dg)w!.P%rujW ^65˕TˈSn'uEP|wwC7LH$4x&NH Nfa2n&8tY(0SyI|Kʢd$ԕf?`Jʥѩسm57/h̹{;xp߫\n"Q>ї)"rUom1}E|*L|4R8hI^"xʞg;0b5EdҦdrnO`{0b `+ w0S; qX{@ uwN=.GK_UYY.Iذz'UԐz IDAT~c#y h7f.h &S@Sڍ7bŞ`n:xps0N#!eo#dفpq 'h0.ihB;B׌P}񒀋ug;vS ؁@b O]=ސ*"CLa߫wg%y Ͷѻ go3:ݽ+9Ǒiv`7ypQq#Mԙ)Hq}7%WM$u]x 7خB9d\%XG`}^dG H`NH0γwM}?kH9Mo=zoO1؀sH10nM/n+c mV֕`MzvSDŽ񿹁Qu3u/E/pH-8m(PŜyĆ1aq{ a0GZ2G_O$tTº1%P>]SWD">-yݯtj$$IE$'ol$ၹd:B=pm0c*rSGh6rvԅLXRkyD:;˩l(&"^zӫo<3+X30 u`0oX]]MfffD|B㺿Cn߾}3?ȋ_`E\wu/0=v^/j) ƨQt _DDDDzi i%{!S0"kq9=ҧowF""""^Zzi\=P\_GV)ODDDDKV/| >\t!^>g!"""^ZK+0_"pw")$g/ | %""""KV`+s+ l+"""^ZK+0G@g 6+ | Q/-ꥯ\]]qlEq_ d/V/^z!Wy`WE."""^ZK_ꃯ:%0"2 gEDDDKzi("<Q/-/uŪkhDDDDDziIELDDDDԗiəty NQazi 2IůbE Df*~=Oi2GST5EK(0Hd^:!FO]N,ak|ZC971Iwo)y=@gf`;jc*\`206 f1$g<ُi+vy;\,"" ""Bnv9o93FA7QEο/ /`P6NL)f @77n 7'\ ,A=Bl)َ #l? I^ouaN*5Mdg_ 8),V%9'SG=o^I_>Gr{Q`dG_k)p}1VQC?g`}.~Ŏf{#̺w,hd$dĝq?O Y#ˋ(cG?9I4?ϊ۟{U婬?tQȹJylEL=kr*w t>d)/׹0GE,y6a67i)^:1-ppvM>?*"?M8ZCX0ӊӶ&Rn޿jF_x7UFbʎ +` vx)k&8`¹d za0:e2u{CS‡Bd2&3c6R':%qD `,zi ]DKS#ݯ"Oz.|{ڳȞ~c'f1I$iT,)HL܄09UN3`vw1qvx1; X|&cO{ G❳Hbldߙj:"""2~B@DD·HᮯU|XMv5ɘSjsSN&2QA_[p6 51fsC̸h üdGfb` } hBJzp53(z,8YB>ct;v;$#$<ÔYXJŒ3ذ$?;6>SM%!І>Nԧcb1l9@>zc=447،?5/dE֥4Vvo$2ɕb߾ 殿bz|W˖R>jhh0zh)o6[bӓ,uW{mDZ2]DDDED3b'O8Cgx'g=e-e;?"qYdrLHh>0wR,c5S-G)x$ȑϯŠowKXgmgvSDD.-""J^L]ΐoAu8kp J;avSQ'h0ro md q>15lXM7zBͶ%T~8Cwؑ XGx .X&SWp2tM\;zF[3@ng{#@3յ011? -~HpAam&t|ﮥm*97tofac0܂1x uZh>Y5fƎO w~> 0~d?u+$y-k,""0ȹ39[)**M2aMg$ P.ٟmb U`:195OOXgX&j9/53H9Mo=zA~yԸLf7 .ƌMƖk3Lys달 :$M'%7_yJts: l8hh˹7VS2aIc(gBvs/ .I?籯|GGeO`;3R~!L^lniN98+Ǔq}xSu7hIdLa~!Vo3\[9dcH=2#V:WV8tx8pluS2;˜1U[>p7 D1\1rH _Kg=4YVwTF+R7m).jѐLv{%y!R*Ӳd ? P/"""Rאly7H`tjH?zhǹ,p۶m|ͪy>8Ą쉌,iQWj,bͥ+ෑk:DD:<9lذYp85|Jg!A橣' ZEDdς-gXs >JrNOMl,QXo©@DD908wK,टxQ` L&xɤ9 wrD@v\&a\yɟӯoO>D{[DDDDDD'|u]wuq1|pۧ=.""""""gw^F^{ufɓ'F~/QDDDDDD3 h4ݻ4i%_ɓ'sBq""""""$ηmƍwYԸq.ۆpU7ӈK] T-j%8m(+vWS0rAz+[y<+pi~1`?׌నDDDDD㲶0[VXY %^KҴ3Oΰ?xĪ|Xٱۭ'F/?\AGRK*VDDDDDfF\%9)z :[Z)#W ͎z/-/ZY[Dww3S9V]oPG,4njߣMvRȂ pZWc]_B~̾/Ά6i}b.P-řj#-o@ ,v2{(*)"/3#_Ău;1(gIV}X5Qf|>E|{0ePX%#mԮigSRܶFڶ lus|fc497uGxy8vf|;MX'fqǽe[v׷ҹ#Ls'\.Cɸn#5ޛC;9&_ѷ/L '2lR2Ϣ*ݧNDDDDDJqe=ke?;}yϷI:Sj F{ &dqni 3;kZw]42|r3t5򩕴SO Q)د1ٌ yu3 .2-]4w ҊTWRF/q̹Cf<@׆_P%ʦl#lS5^Zn%gS^,Y6~[Pk;vŜOx(~Ǝ4ӵ~7XFtkxEgNDDDDDJq0 ֿ ͛~$wg@Ø'himW>lqnȚSF`ts6eO/`Ҽg4ꦩ1v9y-3RoD1_`6b[WcjȺ^b;$p En wI0d"wpm84WiCMDW<,-$lfm|[6s|>,Xz;t2mߤqd!N8gtP97sl*txP[DѾN|;؃8toz`NӼ4(Fܻ1lg~+ȁ`> {Q|gq6q_dd7; ȤiW莝>^1;NB΍RəӺ 2em2I##`qSSm7l@%[fZ&yn5;q9̴7¿3 n ns^ MvOs@Or`&̞weT)ro0[ `9t""""""ߜA@c=GYqb<pOQ'M E9}!0v04 Cc%}L#%klNXg`JMf2j0 R]c7S.1iE"S;K~)PRG„9p%?LijN7uGq=NDDDDDӜ_5)'X$_FI2O|fjkbV] I-sEXW=+^夠BճwstƤr(Ǣ}"F적f6x˙k;B(;s]tde9 ϮuXgg].liZ+z9t""""""+v,.\`=Í{LSc,se Xvw5D,2.¦5lIal7i$??v %n^1wK)]l#D @䈅Te!v휺cGv]N0O/``o-$/˅kl`MƑTsNDDDDDrq定9VubdwN7t<90NEs&bk']{q([bw1ڵ12tHE?;)kN޹Kd[C-iv'ȸk.CָC;y܋t7rHT{((H^PHs`U/ ;D+Fl^33`Hͪyt-de7J^9۬JVmm3&o?9fPrޝKfZ?]j ӾW̢.vXf=HӰt4?fAz$6}pi%sr}[?批W/|i X)Ρi{uU.ʔE3ȷ&|eS0 ܇k/o˅1$~s3מ|kc|I ę n]=QL ?ncv3,gYV,aMaOҼk9S´n\|:pOaG68JtNbwe`!G }fIG|Sg Qu[q/-5ӷBigc5+h6Uyd!Ϯe 'a-,U5TOmVA\(Z IDATGZ1a˖6˩sªȯZE>`tP^>$ie=;̉WS󼕚ظX·c*Ex`ۛǮ}@Ce-""""$yt;: ]c)/Ȼo%6=arE[iꈜ.NW;i{żar}!O>2VhZ*vRPEq .]!=x [S皷<|\Q9݇o{=&de0:sٷ2G/Mv iMྷfzz}Y=Ήcm>?cIx ۗ(cO'G&H1S\'7lcg0WPq}KܙVtbLN^0(^1%;HʕO7uw56>˫h &6+4^_/v:q)x ]c0O=Nv n~5Gg S'C>lH@@3^8kWc˜ȡ'!!^ M|.١7 ͪdw~Ua^|*AEDDD2w}{vMSi+TП[v9KMf7;۰nlWO3ZGGy+juU|6ѷ;O>-ݛmZ1>l9q"k#4);'e15NN'kزW >:O}> 4wG;[w[ϧ2gF5>O,@!F2y[g"ǦmC7tcGZX%zy n;sYZb{SD A+R6<뤥X}4=kV1~NX^V`o +m?̾W/""""_%׃k+h>6rjvk9e:~x `KcCZNs{[7Q|{0.‡+)n[uն0[r '*""""WcF6v#%w&ӡAvٸajh3|z^e1ַjm-xfc_Mk/>W1ZֿRK>j `rrM^6 Xf= + 16{i\a{xژz֯^DV:r!Z^l <5g6|7k\9jJKJ(}K?/dn)u{u͝˲7RfKՆ`mVPRRBI2Gv5Qu\s 5%To _Ҿ7Y$GK)Eu>eU5eenI)O7y@k8bLSbqL3gR~ 3' Q NxhAU΂+78zlAN&07+O0NeC:5Ȼ+ 3`PxAWו+7Clgo6ߑl9͘N:v|s䍵s?[S>Aߛ LAOӳuK(!Du")U<{>E9'oome"\B@zb9˟(Jn }EDDDC,,g[bĎ J)-bņ/0=np}il0.O񮺡z9t15M~s2N0ζ\ \0AJ78؏.֜ k8FJ2q#߬#E},bmpO7=~K!\xxp~ R_ - ʅz:iXȻIW,{vs6P2MMid}9,z5OaR#H쾥nGYfԃE~d>ႇP,ONe6WP#!Ol&&#~RӔ;0[B3fOw+/Cb1LVcmVp˾(Ķ,8#"reOt2h_Yop +ำ}ulgf`Eʹ-""""mf13_9)'z $dSN =~^)N4x, ?{&9OVoe233.z=]e^ &'dx܄mYc’'n#d2]A|c0l$#n6t4Fوgi7+0[,fGlxOcGC$ơ[X~Ewf`ٗ,3[C˙7x1UE˺zַ-ť+""""r]8g@)佪AtXdhsF5K:iɑ./h53UwakG?Q=Ҝ$_e^ ؉'B66uYׅO'Ɏ+h+-[{H gcO;r߁aRݲh!oSœӶi|sc'رaϟ[uhhd`p79AG_8JFV>_iM4L&/+_D(mgXZ`EaYDDDD.,M m[:!MfZK'NgW 7'O Dک{9JO/XX>|8餧3rKT.2/MvZb@Ch0ib< `ѺB͎+j;]h&v:ӗ;_ `i?3n%oaθu>?魵x{qCͦ6^;p5i.ZFtFW r3,4aVof8Y_@h OizXŒ\qDDDDD.B/ֲ|ɜ11s~RFK}Qe'J*fZ M XF2?xg}{鮚K(S6e/@w^<*\]AI |, ke\nӔVR I޽̹~>לULeA-uO\”-Axc-͓]9--WPjX)ɞC)uK C_CyXߴTAH`tjH?zhWY(])I'@wذYp85|JaY9pX0(0e-""""""rQd\x """"""" """"""" """"""" """""""҈KH$Zbٮ|)7NDDDDDD|Kaպaח`> Su1Ko4y ܝqγ|jt0\ QHr'ϭ^^#8RSvP.<=LSgb-{2q۝{i+(X]){M{N|\-̖y,|Iaک̘l\s~B\ """"_fF| R}cj#t.b-m"2q*Cƍ^ozNkom 11T؍.\!,YUVh2βp;Kqhoyܒr20\MM Z.@y 0v77QIjR˝SĪN{9W9Z-)^ LpVքghŃKP~CESOgϧ(礈߂ EZXk[j,0iKHb[SZrG-~{2h_Yop +ำ}ulgf`E*ӱMyLVl fyY,Ɇu\i\?ecI>DuM1δ/wJ~[NfjV-VP94ʼn0xDhDΧqQ_m{?cl@ff&vE˱ W^>cĈw}Wgl!%zAiɾ՟%?0[Vli`Bݙ9  8t̛vԘqEQ*Ge]=m[a\""""rX O 6\?z8'{D>,n*$J?Fa搶4B'ϧ{C';NlG?R]Nֵ_mZ~%viN/2/"=ʰaîuK~\~ַv3o`n=nqS\}(YZ|3/3#'9FoG70=|q+1:_(Qcj1ye v'~=@\]46 Bt0Mab:&}r_ [ԝlP29DwGYNnv`Ƀ;NK_ ѻ}7z>|8餧3rKT.2/ѣGWS־\][3Z7K-.7KO==lj~n/W_8eDWoq,r.' =B3 YXffV,cEdg%دϥ-S Qv;vɭrSFDDDDZTcS ;'ݞ wӼn7E&tRFH@ս͘'bz]%cd⾧ 1.8 +)~k$JLȣ"D:iaakH<K #30ҏ=UV 2JW#zo/lp)ٺOÆéS,5$H>9Ip9~k!B6zy9DDDDň`,rQ`>%q"DDDDlD&KE _KpU)0(0(0(0\Hg"j]DDDDDD΋fz8.""""""" """"""" """"""" """"""" """"""" """"""" """""""WWE^_Ƃ5(XZOYy3Տ7H yoj8~6ϖ\M^F+AkYGfzg>cŃ^gTRI^vS5>M#O2Cg|ĎρXr k4mgIJou4opkOܔ D7K[GM٣mڣ" CXr.̏"8o/KQz]s~Y6\ """"W_r\ph舁kYR[Nx_2{ 祬6{!fT !zi|VF-e-'ec|QKUQ)U,J.fO=X[E9K;0>Fx5PVGL|%Z`o3UM5jƷ; V?Jg`WU' gEl`PW_ފ8{(} eM=-D?`>;6Hq'n`I}E,bq11T؍XF%3Jnמ`q>[:CGbΘ3 x5W^D` `J|8wL/a&SO| oA~CMu(rt7ڬr0<_E%9Z-)^ LpVքghŃKP~CESOgϧ(>'-xPē+1|=BVO6T,g8C[iԍqu9b0\KK)q5ӘFB6]:W"or6M=Y |q鶾&rY IDATLFfc+XR8n7-[N.""""r#ӴR*Ѿgl<߇Ct742mbal2]F}G+"x !A?DxxdQai|-s~479n=-Osq1H]3̥|nyןda!BwSx cW']a8$" bΡ@ a$ }l:cwŸaj>^ƕ_gf!Od=ep8<(ͅ9& ' *{#(+qc,4yKki]Lj^ƈ84\O{O>O0 9a5Lq9afL8u"wɎ 0YMYmtgo%J$-)y-9H8Sʰ2eѾ|Vqg9hRX'e(OF0Ϭ~n tlckbe|eXCX YETOu5Jd?d^KiR>e麧/w%t58gS~\I=yK ͆iY;x wҵӠp KZ'F'Ms.Dp| 7}wv!;9`?Gd]նkG?QriXPTTѣG6lUYsuɻdO9m};o7~Ͱϭ}4t208n /[#+]/|ef4?g/new"F eTж3 cX-f09=+!@7un V7Sg+(-c7i6ט!m fvPz|@ Bumb:wPۘ*gzi"+eoZi Yn2GgɉuSggSɞf";|#r,tnn'dFv y7;0l/ξ=d~ >t9r *aѣIOOg)k_s.y sƭA Moȥݛ%G˧j6Qb?܁PHsQ27Ҹ9wa,z}3+ƱϢG[e}H3fRs8ݝՖQa?NSXx(#l3G-~A\=Ng\xAM{Rϒ\ʗYF/k:Ô</sh7vSlrSO-Ht{w򟪧&F7Eaf)ɇe}$u{KS=l """"r"#eF²UDl*ƭevj\."""""""%~.U"""""""_,"""""","""""","""""",""""""r!]򟕊D"u9/6 ̗rDDDDDDDΗd(0(0(0(0(0(0\F\y} `i=eYc4K{Q@XFZGg?TU7a*j:t\R!Z=BV<;1d3*yڃ9·n5M~t9?\Ʈuml އ)qh{=MXAN@d{Ͻٔ=Zۦ*<4c12&]h扵GY14PSH)-&jJ~9f8m`e&LĎ8)ٻ訪CoHf2fL¬#%}JD+h%"i5zov! #_Rk%5)@5z &/&;A I!tRȄL&|"#P!Zg9g}>O_b[3r` S!w _[Ju+A/1?XLŝ6 _%KYMmbr| Rօ|U)u?]J՘ZG ܲTGgEiLfG5̏+ZF8PSEݾHr1屧o,X\-o oYYC/p ZJ%?]@ 3Ɉ cbj>J/THGR:sW i?ǑVaotkO*ُ-dk\@' 9̥O U­QḵT}0#HoNei#>wd=Cg734)h]SsωlgE.U9լ~ 9̟yK&XRl9w1̿ϕSx\yf;kI[57|?ZliZ_`v.\}3k{0.Ԗ̘\3[T-yy`U-AtԳ͜I8aO+h_;7Tq'ߔOŜ‡H4E.""""ra_ZDcoc8}$R| S{ mL6nglHh5:H2nmdzxL'}مz4Gۇhk؏uhNŏzUr\q.r%y;x3 ll ٱ7Ӹ̟޻AzZ +91 R= 8VTѤhK3?ߛJfr26„#&L 4LaN ̘La:U"0HM+%ـA MKztӑ-?qO1@? %;Un$ Qp?Y˩TA`܂yyOFȘ^M@Ɏ~;v+f $E JESjAϧ~Ͻ8ug1~} 85=Ux3ŒcԭYUVVgH,u0j)Y>~3cr5V3;8 aכStnGO HghB8~}!O=5 8 _Hd[Ccl80h%3Nn j^&;3%U'dD|F}|N;1)J^(dv$Rde[z׸X ؒpɭLZF qoW*H?,"x t'bGD["@p菿|]T\"w EDDDDb9 "! d,:""""WfY: "%>%[DDDDDDrJ@BOR`Q`Q`Q`4]?+tEDDDDDln`'""""""r4%[DDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDDDYDDDDDD<U,XT ,f:(\FU?8.ӷVR<ӡWDDD意XFؔ[̟| v/oI|/M?Oλ]_~Ļך񇓸e,&%;TGs耡=QČ4kg9n$9/dq\ə-ZȜv눈|֗.UfEƆR|ݱ:*^h'x7el::}rz]'cZJ7xg :hک~q XWX*i2z`lZ )_e=/{ַ7al Sw~X?k?ߦfvbʷTGJ5K9Nְ,ŮFߦboC"Ϟ4Q1o&3i <6 X .f?SCHKKXNg!gfv28u4l򿿂]zE|4z 3`&oAC\f[+x ~ }n뢟yىM%k(0A38tλvGi<#S\ݟoNd,gвWwVMiF V,$ۨjO?|l>SGiy}-5>F~#u Hڽȅ@9x6瑰tc:_7?_fZxWIYHGbc| ]Әǜdg˩O/eml|?x.d3 WE%"""rjh agcBj'DgOο]_H̄5w`qíh1Qj?i}T2nvsv:6XOa%<2Fɻׅ^!ǝŌ{]#~ZwX,Y̚EEZprĚ6lgfЇ~N3As:S2L36)9;{{h< yq=5G"a}^Z٘ l8a\0 S_&pu1tuzK}rNWܫ&&#| lw"as4c6(ܿGOJ:$vذA. %@$ S%`gNf+6ۙ2mqp $)vrb+pYAC)J;MnJ!\A"ܴٯG2"`:gP87(os(bw2=KDDDd6a 6/mF8dKd7a6?״?M6ʽ{OGy%'.EJzZ6n` e`an0%~s@IB`%{rs泅ġ`8iք @ +3y&%ӧOWFOMlf03sgV7*$lq`Bߟz܏,lvMl]_AeZj'7\\8i= Vz}E;mȈ:F[N\nFoG:+}rЗ_Yhmo)^ZO퐕)˧d4n\Kz*POvoO%e\Ns:4/Ƚ qA7UR5麌V.gZjXKtK ש7tG/ /[|tmacO(-n%fmRjؼ (ߺ8fKlFDDDDY2o;dMp[.lܭT7 Mj986Adh8NP: ӊq.t]\;\ߍN3j/-/5> ڐ1{\gbKdV29YfHcaa.#ڪXOlI@yVIޠ|0![{5NV^'<6o#/I#g4 aMo%K)Q:=uq.flkkW,dC86is@K?aeTN!w\&C""""td+b%]" DZu%umΚEA/2B=$ؼl{(gt=!qߟVS3pl+ Sf:ĖޯOqDnh~0($Pt֡cșWȌQ6̣fܸSEDDDDDDJ'd)$ffffffffffffffQ`Q`Q`Q`MԬH./mb`E5e` +uW_ĂW|0l*Kq]9i/4-XY2G_vjb+UK)-"""""rfWͲE˨xÃpLVP2ȧ_o~pkMil?ke;5Yj IDAT1dwU׸e#Uϭe3 eZK٪Z|kYq,ie6c`￳"k56Ժ@38;wj̷bEDDDDD\Ch0|rGc+80q;@|c}RZGО G=T;pe2gv.H-/)1sx~E)@aOG$%cC̹4Q>oL|R(rg+'ُ.dmmTpc:yO1#c;3ϱS55ad~}G%k_GA?A8Ҳ?̀A]6yd)9rVc0:|iA5^K/ IN;gB{Lp=kJײywd~81C kY#aLI/`G"""""r S[ 9Q`)g3c{WP~̣ɽ/L{'MoTsL2scIL$0װ jwIte3хh/-c^ ̘`ra'd3FUxŅFP:R>*dh^0Mh]tlcWpF~m*Y瑱|j<'bm^GZ6Bꨑ+OMf2lR>^ ś>95x-.oI>V8izy +^c?.|s 6~Z?9kUab覔XtxiiA/L~H"cB)q,u4x?0j3qiv#řklztaNR<;3qlHks#^#:auKt=Ytϩ $lCPFN&{fz݈ABg quǩ{Sp`?OLot*ŁcpQ -t؋ G\f|m +QN̡͜*ښ{GQ4<tFD{}?JA0.&˩nMLѯXqf&}]dHk[f=.P=k*^eVg ="0zwC5;zD51!qc2BOBʟ|~.DDDDDD>OّŽCnWǣR,|Y2$Raw >\kZJyizV֬bUJb`t; Eðux"U J.B;Uϖq?$\|T?[AC9xP_8-EJ Vw; Ӻ::XI8f`X2O9,ۨ>P0ȵ\rǘ?5e>ȧ oSa/&̘&Fào ѱwe=fɇ5E<{3hm#O b:ht:CE+swf∃Ucӱ1|{Jw$u._蹻9ݸ8sVY4h?ܛtr?t1 ;ax7N V,?""""""׈~Aޓs>SN7b<=8i p0f & /-\ΒW8'rfcf~w7[ o`?O?GFz!U 0pُ>Ex`j~av{8{iԏ]ӝVPj==fv7ep/ANf _b1I=%""""rM#́w_c.(OzeB`Aߝ>=`w([us 1oJセ=нMxpӟZa71qw1odτl]n`5o3_3f^kG2oh#Ud/\HSs{3> ̟˼G'6ec7b%o<|M.2nmwRXNEY%jP ey\G[{ij,|җq(++c6*^%BǮ}Y?g~7`xx%# K)+-fn.80wzcҰ #p٢>ÞN'و8!>r0: m˨11ALfD<?:7mN,iM|Hϰ[ےH4_䌉8-D:9g6F'ڰwҩs3T3`%cb&Y+Vg"x6g6v FXMZ_/H. pc5TN3'1n"s($hdn ٸ>kUˠ߱X,BBeəFuw3+V2˩@1B!ö׷H7C#+gX.t;{} 'B!wczNoU5)a  $tbкu-kF?iD']`rk3>WØOV ؉ZU37X`6YLg?u^uXZW"""""laB3-wvvFd'$bIKނfffffffffffffϘM)xse ߻EkE.c߈TG""""" """""""װ_e'{jxs|)S$C/`pߖwr^Wmv;JμBN.-g`K KgVZ_8-,A}`4GVJ&'Wƒ4?cNJ!}|]i/v ̡` \C[Ci:`Ŭm]K(=E9vyýn3sh&sB˝ݛYXv?+2щĿtHKq$e(,E׮*2Ls~UUw)eeϋkoǻ#^w6adݻN`Vq+K~ԋ?6;ՔN2ʊPK0">*YMCTTؽ UJ{=C k'0 \J²HΘ L$m{D@FDm;4un_ "fв{?3I;&<2Ƚ+3ͣ&=F/8257lF3 v&|2^fi5)hF/ٓpsOL͡ &fӞla-Y?ep;p9A? """""WU͉PCoطzN6'ܫiMi%ž8{v:$7bp"!1:c({0ApgO GhJbphߴ~#s˜VN@bގ`Ą3e'& $hJ8O O&)!B Vqi@`gPw?܉_~ɤ,r>Ӳ5[!u#j kR62beŎ&2M$eMNwBV~5:Qlp(蠓$J`;O?$`j%xHa$ e_is30_>ȕtdI29n5iiX|ؾ7/8ǤҶpC펎=ܸMln=P\%XuLڼRL6[ll6fH"[OCDW$Zkm[4ays:&jKX!;d=6r ^"""""r%UfLWW%LCק%ўlI:'oS ]Ucg0y+:%;Ɍ'gP ~`@!3bܜIT%a<ɣ`R1e?(2iIY~`{.@SAQ"ׄ1%gT.6zm!Y7hZB=1LGƍxg5k$ #t~+ãE^EW0kd=f[iQ;]4mX)++sqyh: 岼M&WOr'il[XFZÑT7co;v1:mDt:ц-.`|17Dy|Hϰ[Xx[qBtk1HX'!k 3=:=;ٸb[`W3mN&{xzM4=wQ1qv7pM ` tڶ5H&c qp"f0d 1hٽVwa5."""""&ql,'O8k]|PrÉ7:SklAx mt?vB8^Mɻ;⭑k_0aIab]`NLV+^l0`x/ qvr `623cLᶎ&*~c|\qC?pz6r+,;}f[~]i`aO0wb +p"@0vY' H%Z|tu}&{lUcuBu qOVKr֥d:S-ҲH뿱ћF0!#M쉮J5ᇚxׁ]q8\^U'B!Dd[!wI2j=׍/:qFhhomlnoRǾt҇jfaY1]Jfܓ2wq5 @˨oN#˜qImGp}ȕ#4&߷a%% -щnS;=@MmIDATr'ɗY-NG~urݓn3'?oeXnUU '/iɺxFJ+bclI; G\ gQj!zj*:߅3FxV-`@"#&Tn. R~x a.odDDDDDӀ5DGľłu<`nŖ'Oni9Eed1k,kae&]`ݱ%BU'ZNc\I-N Q*ּ}3pOS}<9?%EgDDDDD交)"""""" -""""""S`Q`Q`Q`Q`Q`Q`Q`Q`Q`Q`Q`9S'"""""" ̟bpԭ{>g%qĞcT饭ES9ǺkXP_W,9[[=3rϧQj f3.tCk$UO+@CckͮF`d{}JۻA]#F&{O m6ܮ9e1kE^<[ ɎW|t%lصu׈ȭuw}?gxLgBGɄ8- a3d沴aM~g}7?;r-?ƒ3oq0\ŚFuQYHwPH߂йx.4"?KwmNvŒ=@v4r{A/Ad+q,C[B,N9 n`vQv>gnb?}CɣON=Q9AGހ[jAQvis,\\?;}_;I_Ђ*s|'blcA>j;,)*g1h|Y>GsŸlfS kh&Φ8ܼϒvVlj},;hj ]_`3kylFV;cukf -{j~euQ^ڋKQ 7WP37ˇcF{+^G9ԿccU { t ژ_]fzisƕb.* ˣŹXxqͮ8C= Ox6ͯ7 Xҡ~tv<ˆ^xaG/1|{nvmto8ag~O3{vp9'i+e]*cG{ޠ>muxTxל]U8,tX# 8SBѷm߄'[32\']Si586wog8E׀%vLb g] K߽el%UcZ`g=#K)T~ "w/v֎ `i/-nc̩@C⥐sԅ3Oq kz*h,PXr\uOg4O/Hy/?-N 1p ,_6'r{*p S8\fv*&uso許}v 2 ̱sذ%l6q{Ň2Z3}i\2K9fkLHXǸ؊gvy0 Y,Imsذ[)0ƺĴ`ъEAi&BhT dM&Z)M<]`CyM@ȇ`bŚiŚUDզ*d !:ϸ \xf;k[Z+-`,02ۚim uRJndZvU6,E~'/6fPfi0{f ~dlvfرe@a\yH#W;`'7ϋmzc ,6݋""""rk0_Wܰ@΄#MY3﬇S_EQgg㘑:ߤ-»bv=sS|foqtLqƆF41gl Z,8oM,E%8bC/qu9fk^/-;y0A NnsxrCGX Շ,^ʹm쭭 }ӖsJ(5Rp✓X"-1zCݦU,g_9ኧ(Ygr$ޟYGQ- *^v;Kv!>ªGfdz>dF.UO=m=?s!s>CE*voU c\#2JvʺE>jVrC?1e/M,{ŠT̍o"ьwkaOTP8@ΎT"""""7DZZZp9_t !`(hlIpMEF4O88DžclH"""""""S=0džDWeD+d~=RvGV6Ǽ6[2iRJ0u(fdlU<Տ*0:MK51K<-NDX!YDDDDDD&T9{Bːz3$^+Da9=&EDDDDD$QM>O8Ih=V*"ئ #$ #'[ƝjtYDDDDDDzu(snɞίЦ$*'8tʵӓefс$9>$Os9̩RXcpp{x;UGcmɎ!""""""r#r2p< GpSed9Ѽd  FQVPo:8s-ay26UiʯEDDDDDddyRay6mn@"""""""@PPXȘ(T EDDDDDV 7%*ȭoZUh.,,""""""EP ~ޭIENDB`icingadb-web-1.4.0/doc/res/view-switcher-preview.png000066400000000000000000003115241516124654200223660ustar00rootroot00000000000000PNG  IHDR +zTXtRaw profile type exifxڭgrcc^rXbw;xxZ"E!p?RTjn9[e0D$ڋ\ ب+5SH1SI5s1s^B%\J^C5\KڛopL-jkw&ܹއađFeFό3<ˬfٗ_a+jo)wy]wPj'xɧzYsIo?>k53qi)!$)gdGGƋ2@A{V7Jrf+gIYN#q;77ΜQo6s36TPm;3{aK92HԖղ֨.0AX~L{hҹ\{ɝ &[A+oF)iq9T_i\9W8Vrwe ۑכF ]NגՓS_PSdżK01pNDa\%N_biΑBNȦڱÇhQuW3Uڰzc=ic$ԟk;1'QմR$6:ȯi{XpMcn͟;tL}t->ײOon;}5~msۙ#7)jV[.ʲ{6[T]\[g3(b x/s@xf}taZ\w0s y;/\ǜyFio6NeNv)u uAv/g؁tbQ3\fj֚9:(vs,黾{ϼm-%0yM17ybԳv\c9Td5q?:r/X4栈|D#3R q*KԨyVD15g0iGo[?^Pi {E^;[?A>.<ltWf%+F;} Ն=;C3,b.^b6֋1]w2툈v>>U˥]Aj+ 0=bS|ȋoFk{CgEtl*#z-|K>X95 }E{ATº]xj;xˎŜPdB lPZq%h( t§:Mn`$#k`=c-,,8py*{} A;t{R_م("UGb`Ad`WKhaUpA9D}W"jQ5#6@ǥ@b[ Ѓ.7C-}R#c{Ԑ1Jk< WG/DeiZ97j^Th`ጟ'([n|~ p7yBJĢ|aǭg' N!Gma {JvbyBEȁD*.KWхk|4,Ơ|i@{z>҈Yf*7+ Q?oI Fl-ptR%.M/ r)ڧ$N :e@ݳ6*ű:2f׍/}W3`}[ m{M$MjRHlA~7T0e0:ZI G{nNfK( r!~j7w'Y1\YabI&f!kZrRɾMDNӣ/cmuf\<P-rC425ҪDnA1ygJZV,N`9'/Go2o*rCC)^q3XRj*`9,ލSj B1rW4n= ҽH-E'H6\<Z y5QD iHPEc*da+R6^ ܬkgOn!"@lp\;b.U)#ZENz">XA EVc@!{JR CR5-@VwYLnDI"O h $H/Ke< mY.EPDŽ;QI e[tv)C Y J WF<ɪ̄z/=Ғ%`lo9\nAWҞnEw$ fΔAs{B%XH.N "ق-BufQyռFN quM[^PVo d"hM t&4FeVC҈|ktf+ǃv'A2mtء`„uwe2qw!U9mv)&g@-…\C pvD6x.;>a]Y R'^ˍړs}A`qtR^=`SЬ;:sЗqxX114?Ǘ8]yp"lA- j ີh 8C9Lfa&$Tc8֊J GuqYxPSCuqrRHAM&VTV{5 чӡ(It&g"ș ]$ry.@S 1`dh]T9:9D:fȃ_p^,ߠ4'+( 7C%C|=bKaPH1  phjhJHfb'6UIY {Y5C~qiqR넓^^гZxSd**AKM׿XDjudu' TIuQOS\"52Lـ!40"Jv_AK; au?BS yq{VMSSo^dNT+&;>מ"*͐b'pC @bѝE'x@W4 ByDɐncLG aT*'%Эtd{ p]=*;Mrp{Я >-`C$Qy"􁘨{؉kbޅRS'Q;n%uj% -i$8hbBg׮@5)-})ʩ{r\,-x fz 嬡 ˎ<@teyh֕H#Y6 Z2~C(HTAl#0 *":z#<<*@U;V1['GwƲN΅fн 0E!q/oľ4f^PIɈ}Qs)J&%%S-]19}ݤs HMr jF= J}[s:;W_|;:r3d3(2GȗQt"qafsY: jiCCPICC profile(}=H@߶J8AD$Cu ZQP Vh?hҐ8 .κ: ]Rh=/wzfjJąLvUf/bωb>EywݟOə ijL7, MKOfEI!>'7ď\]~\p3F:5O& m,1+*q83.+8*kޓ0VNk ,b "Ȩ2,DiH1U#*P!9~?[3?5&@m]QcngJk+u`ZKuK`I ɑ<~Fߔn5os>iU88 [r#ҷbKGD pHYs  tIME  yY IDATxy|TW2K6L IlTE\R^ {+x{^ioUj-XDe\BM`@H @2K#&@Bs~j*8y7Z 0DDD. Z4g6{fލ """+x5W`! m44xul0;ےBV_ٝ14}٨ϝPLL ""\䲑͍Nh"FZvfoEmC~'煉32HВ  ""ұà eeoRh|ń x : 2Hps؜'0GX ^"""}("""/ʘhK|k0,frCP 9ɻ`I-!a?O!WBDz ^ P&P&P&Q&ү}I3sU;mm. oi⥩M@lRYTbrĴɋfh+`{2/4 g-BLQ3g Q~i%{*Z3ZxYldpAt Qu~jo__+q5( G!֟lڛ⹵O9Nr/) ?W&"r eeeeK.(xA{'% `-$Y1ڥ;yl!s#{1O} I?1,-䩘bR=C8;0 }(lzkn!  El9Ld ςV(Neo$}P6}l!& >K,$!v\;7PfLJdRB]dI^ʭ=a'9gY' zwŒC84 DD+#z L~f{~7:8z,Hb&>JPkwg ;0paUvfL4'b2SC__."cDÆ&~Pr81[CF%W~n2M90ow|;)""""Bx0& c¸8͜lG1|t >wLSy0lHK_swGyO20Ea Q 2셗xL\>gXv扷1 x3o6bTi0>:Zvb-?hBVBXX(7;O(((ȹ.7`h(<3_]]gѦLd@-|i` cWE9{grxID:*jprRg2A@@DDLn j37G(xikuT/ҴHDDs^WW&P&N /s""" """"@DDDDDDDZKi5DDDDDDDTPVSAADDDDDDDZMi5/νә߽~ f|]]V5HF@Uípg2A@DDFQ.8s;[8iDDDDDDDTPVSAADDDDDDDZMi5@eB>U.3,Ȉv'mF@֠6^]]Ee唖;u'8B@:_WWGIyjk1y{{`Ƹ ۧS[KYGU5NvnڞYD@@ TPֆ}٬eWC1CqY}-Daq {<dB:2:]RFR+k'P[[KqY9Z]C jm:j)pD@@ ]:A:FUm_kUVhd*vuRR#((2A\3N7)| fP]2*xZ"ҒHjjFDZCgr]s.R8_V~6]WQSnҥ NtB.tw\KQaF⭐]N|ᏟVιN]C||T|c̍~!3㯅>\k(_@D:A&PAA:p[ȮN j XM5Z m2 yA(RO:p1{I侽f252: 60~}l[` gwf2"ґrAhfx >GCڸ2Xӧm#N Ha o4#`۟sMm@UYI<>. Hmv)ey=8?+'{">Iևۻ[Z7Z#n 5Li7􀹥~[_&2!邥~$;:OI'd-x/J7Κ ({Xw w-FLgڝ1?{߽. 9$6GS((( wo~x=7ɍ΀lfv8kh8?3b_EBaD8i[2fwkiM%#i\.(|s[mpXHk:a;k0p3>w2]G?]Cw<<B Y̬6<o⵿oH5<_|:CTFw2]%=w=,|Hw<ϕލ뾑) k{k*UG(K䗷uhaibEa(lSߛG_u H x+xo=,[<[L2:._<|ϓU.z)?=@*(H<_lCCL `VkahoKTMj2=UW.,?Xšeb~?xĖm6qnVɑ*Mq| i:q& I?X@Rr>^K1`b.!(!C8GyLhnfu±y+L 9BC1aI7hՅ 1_Wп7E:ŋ W7oB?|kq:תo`"tiC6?;\$);5V>t cI&0?c} 4~X\jڅ1jA@@@tL᭡,g_v^U~#2O;={Sq{_ndSd4w _F+ؾ1g1xG'dk#˶`=QkJr7ڟ7R^e6Ae3{ AYJ)/ZQPȫj~-<τ7&A7&4|(63< H8I?x5u CE  N4j3x=FT.z\Eet23)tv&2zUmwxF-g'>2(~w<64ʶڋzM\#5I)#`&??oȯ_ Ưj;{fx䍙ErO;}@z`E%(8\jj ٰۗ_n7غ`:ը0FӺ<41qw'w"L@Qh"2s,W4_0!,7aNHV3"'$EkiJj$`'aCաM0´(( 4e2錉oϜ222 RTy ^luS߄F3V}U% D@A C2 NrDY9m1cIC`U&クJ>+ r[~LKT2zǖC$ХWL-KY^ƐăvĤHmc}mI]8<@T:~1N k7+>qy}%|%\Ї{a䞗E1r l]DآXSۦ_ feGg?XF`46"A񡸏㵿_ΤO~ TPNWQ!7%5\CyEՅ*/#~,/cۜ0W>+ˏw(ߛs3:3:{rHJ6$Ŭ9Aqh%+=3mhx˧ޟgǀ^.601 2 )t`J5~uֺ b3x 10y"lIVn VodLImD.!ib%TyǠ8_`o(l;zXIQ~%_7\d܄z}}Kk"zs79Q|5z@?$#u=wsk]ds{L2~?)gt2F# _F0iw8/RWMI ǫ .A3Pu ^؏?z~ #3La(r }An oϜ222 ;xW!|SקF/6z^gfw+ŘȱfJ,ߛIb+H\} + GoS09|܄mh(}kؾώW |Bn`D+w4 wV&kxvU?Op98,Z586Wy舙b;{c%^&#<,b sa-ft29ɣ.g j'3N+h<31 !|y>+i6?^SfOoH $F,aMߑD];e&./O 00 VK6at;Dt>q xx<~:=ylY3!O;bHɟ &1HۢvY_`ΠyLi<|uNy߈|+G|~GΜGAF׷E)乻pb-N4E_nLkv>)OOC/I F^ _+\:b&(JΑ 4|w/?r0~`Mon&:y3uPo%jl_WWUi/6+-瑉kKΡPѲ=sX;Wn6UNoeۨ{"^ٺxѹwTe/9'u-_qm$y$u6-crתStdp{?|0֊=^^^#J/k_NE~5ܼe32]Sۧdz[ MuKΡӲoع'yi&gIKwvL+6*(\(((((\Ln j3\LQʾ"lxSD8dj<BXs+h! 䆝K~k[Ki^}~]zw_owבֿSH'*<͚/ UY 4sj}_Q\$(T@ d1LIA%ûW{L9@,CgpADGn|͘"{ӿfdGubX D SAA:7n5HM`Y "rlE]}W٤t FAL " r͸-A',~tZP@=׬mn;j  DPAAwԃ!1hCFkP֠6K5  g: b4jsi>Fol!jvv]}tAړLL 8]6RDD錗Ve]6R/i5DDDDDDDTPVSAADDDDDDDZO멭tq)UN'O\57~f3` ު+Nl9 IDAT22AkkAI~QB\_h=TTUSt- >fE@DD Z]K/eJ&CDYC L ""#USSuWU&Ѷ`%tN522XP4ϊbvݎ8_+¨%""Wo7L ""VA۫TQC`+bUUylvuOD3O LDDd_L ""Lvivԁ?bJ7opU^}Y9q+Ɗ%/{/j8Ѽkk)pe{dŧy޷MVm-R9Y~{UjM̷/YioIZ6,"~1i/mݓ1G7U3(((2AW}`Qq5@Xn{*\rL0a~Wͻ8}ij>Rw%o]$u«_LUDD\mv'P[3K)f<SQXKL@,Wb5ؿ̣և^G*((2AW ĆC$Mm-N)zf2<pgi݋#aუ {~~|7'zcEs6&TeuR> {m: Pk-w1OؚU!ΛmHX*D 1F0dh-0#}7*m# 08n,5٤mO2a#kB?.-5z`GX|o>ECuc`gW/<0†;k-oeq>Yq!ޏw4QR7'k5 kɝ|a*`X*ɇ{8<.Wcc_5C`@6"BnZ:׹C!3wLcS.\5P!gObU@@@D :T& d9y t?{uPtLl? k3N!79Rཌ',ck$u3R}f0LoKRkq r=Rx`ڷqXϴzm64^zݜ:|I©> pw|jhz+r`.3x``0'?]Ʃ`*3>bC0Ĕc।VD ǡ=#(9`W7 7Mǽ߹ "Ά Qx/H)c]ۨa PUv"]dHX Ƿlp:^n>Ugw͗1 RRNYDYn(sQ݂е_<2ڣ]u ֪ ߬jCnuķWCޛv}c7!=(A=VhJJWM LIq]}G`*W]H!:^J,o0xpF ŧK P@4AQ oJ3Co'xn=0/\hq_ҍ[Ǯ((((2AW? 9wO`3E1.~UFAڈM\ʀ!oH6w$q3>zO_zFvoPO>ɦ007` 5x|܋dhqV`ikn)]M7hgeyvXc ٵ㔙{?yV>0*5jڲةLgͬr~{h*ҙ wXLƳE|^F.sQp0EEie6Dp8b;x1A@@@D :^&&aQf&Z+]s׀`c]QmYTPSkۋpםYyd}ڭyqQv.?*d40ܺE{s#-x(zSs]x '(k~و k:u Uͩ:dnMJ*k X7'ʦSZ(&s녃ufPˮ<>7lJG8.+w1qtx~n#F5>~ %l-܉K}'~{o0֟l$mA]MͤE Gx; 0xgw(9Lvw]D7^̘[|~9iDϡH hknqE/E<{``ORc\?Ч> n&(2CWvV݂pCH{Z1llhfb/xqCp#{s! 5@VD ~OAu)7~k+27z1mߊW",FF@@@D :F&j5G.x544nuuu[[:9zrZY~Ao0OB3_sx5x"f6""F行Vt|E9a]=|(o"{F3#)0h`WB.5U}Ɂ\"^^^#JV8nYpjd2(\י(6gn =xĿ⠐6 dB w]%DDS2rkRȲBUP #7%aP6CcT9wz""Lpd2BADDkވsxpT8qy iϋ?_jߡG((C&F(\nN昌 DDDSev=竵SDDI"""Z] ! :\[ oB""LL ړ{{{FB\!ϗ0/)22AkU Bjyޔ DDD:4^DDDDDDDTPVSAADDDDDDDZMi>)cqqZQDDD DDD:.(X:;(t6:ADDDDDDDZMi5DDDDDDDTPVSAADDDDDDDZMi5DDDDDDDTPVSAADDDDDDDZMi5DDDDDDDTPVSAADDDDDDDZMi5BjqLjrr/.CKKDDΦ*+jrqf`OT|zDl ""ry5 #~/<35^(Zu/.&eZb"""{y*[ .""r$rܧO񥩛pȮSZP(ZEń39[GIZɏyb\zD1k;XʛۏI>iI,w?m}iɖ=r?K~6Y>ˮ^?a klc+7g=ZTY?G3ְܰkxpMm+/m#$ [`K[NMwyo3J7;~&_+6m̷#r\>}?=G'ka ళof1 91g3m2FdS>a9TL}G5= &y\ö3ѥ%OgkItNQɤ<2HQSD\>Oy5W,'pi9KF Vb=Xμe I?w5diJj/zCEXl& ^b4?;D˖wVwY\δ|UOJ&Rģ~bO[_[L*p3?Ww4O 236 7?u$/{87ٱٙ>hCE/5cRo"SǞȳ n3`g>;^ XY$Ġy#>Uei,ko\HJh3 m>LCl |+Q&Vyy1!y\m!F(ı񗏱ږ„-=6|i2R{ 0%gg-Lyd -b;{ɩ65as*ߎYbco0sX\ 3Mب?XF瑚TՆ3{k{spxۈaM,`xO3էc&lR)צKq1Hu J+Q@O0ɿFpb K^YɖCczfM{3<붐k'"%ΌӾ/μJeg3&ѦۓΖF!0dw<)rظ9ːG%o='Fo//bgv*M6?'|Nۿ쪸 \ (w,}%=}vO9<;iِz W}r&I1%ϾB}F3s+bԜqXթvFp{G0)$4 >6R<|.413gwYh|DVþ;mLOtfoW-@He܌!v$2CIZ/ ;{n ýgˎMy `b/m( T#uhb.27+3SVDJNŅ()3Fx|3MwSR aF789keO^ͪF3qlzw3'Pm$f=nlz,~ G1㶆9k( ӡ6Evb1 [6מ[aTtێƨzpb5/~y#MfՒ/ 1R^V©FOH1־\7n|¸᝝`tC`"Ayh!KIrɶQh;8ܾ$LLaT/Y$!sk 6#EXC3QlK+ +ހmSRÃW nzgAKsh6j>MѺ=`k74Wg{b p3 f7OO̿Xp<a៦‚GQ1EL }ų,0 [ ̞DگZv:`9d˥*;K^bN;%7rsC!ofoAߛϼQW&X؛`ζL*MkPkGr \ #>mf= rgY `|B1kwE1$`sٔULLظ7/d5p$p>`FRMxfRLC05z. v\(zK>[޷ݹk"Ӟ\÷!T`&t{O%͑0 )\YWU܌,b6="L `kKt~v1`&Ol]&;8d <`&#01q[1}eۗzI4߫ѝIL-IV~~B 2Sxbz/o!xc|x%s 88S=&f_6e.@>k^X̦}E@ y;yq lIk_\NzY6R(1W"&3fPr\I֥<='cj-kw;pH%),CH[-ev9[ w񇿽s rlhm̧q:Qȶ (>(3oq%5jBCF f %}[{s9w/wxq^2,811sx旣[[_OgǿҨ4_}Oodڌf^A56G<:G̼3 WEuWHNv1qHf :[q2Ld{ͨOwQ$ I&1g3[ xcU?f=_j3n$R8D;;"㦂tcg25e)ٴ FЌNe 5 MlM6'PUː3:u*{I/_'~7))Sgx7jXF ;bŒ2c}A9,NEÌi3)&x2$.^0>έ^s6qS2m_l=JJ%3Ů,7Cr3#{(/2)H{~;0.(4]{w#!/V'!.3ѬGyt[F""],ȷvr/tdgSiSLt&j[͵S@qț6>VE RY>9Z@(l2QL|Io/bNw=b}{m^ʢuANpkI\|@33S?(?Nl"}mNob٪43{rL;Md$W_evQVKܮec1&fVsO.54~{SIErPF`nYwJKnNn+ÁC7p,1O{4dLp,c&Jʠni'yd$Z͛QEXI\x\guV嵕%*[j?;`) bS}d1 23o 91&yrXdD̃1{9|` IpHzŹC00aG:I1GN'* IDATJvj8!qСQƲD(!ʐJEyxzeSH_Y o{ꏉlQI'xgrL:wqQU`dPgT0GÉK*P]qS2]q3-MRtwUwZB҂ XAZ : 8 ( |<.3ܹs9y9`"-)^j] `aPXI*kr341?蔙8F9ŗ DVcjjTh%7-Pn:~t헺 +~Z`b>AL[~kY;"OZHDR7-ڠҋ<:R?*k1g___W+:lਡ| %X@1wBEf:@F-ES)RȨ9kaǰ77,GP[C)W+RP_@*v0À1L(K$j~ ?nۊ<?{A_)63 qmHっiWzIٰi?j6܏2?B7ƨWw5_vzz &竣t})^A胭${F0cifC!7:?:J^0^GY=QE+6qޡDnp-SBgGXJn!(ψtR1:"0*p?;k#UU?}Ж mj(/<,/W>hd.QN@]E5dv ֢ޓKs`2AZ/}t>,m&{4f>jc ƱQ#i!lVP"+p,& ^ˑy6vךDwX[H[ǔ5Te|QEi!(ˆ`I΁1S_\ڭ/^`B}6^Ȧōķ(XL@ݧ2f@/Z C.5N[קe;3^NdȑI8eRWtReM^LV,NrɠC\6a ;?3H4[u3MZz٣h[>sz oX=𢺥i[Ưr7q磥>LN0` kYUYpy*,PZ @J(>b"6N:[LYKO.9VJWC^=>;ڰyxv;45gM)™ u f vea}7X NH~N`1ͦjn(n GȠU6}t ",Tk0bعGHf;]!7Ӆ96zSZݝvQ5WfS 9~_* kH?pĭ7V*=td+hROOfcz&aSɪ0`7RVfr}zRM{.=} 7{  ][):S? #G1AD]cH b6rUzKt5J2ZڙoV,il.%@1&~D yjV*i*Lt:M,F#U[U[C .0򖑝-(T]֌E)LW\NJj^`5f-E+.ژXISNm才z)>"W0u8- gI畮(ᦢ^ cAi.lw87Cn!6Õ`355ޣ°$EMWHɧڼR 玠?aİS5vOdl♿)bZ(puTUƢes`TwE7O&bLvoe}3>#)gp N$]n#b?KI6&|6di8-z?9 B!C?rvCKY hzl \MmSfLXǖg0ƠebAf/,34I֑v#O#KǛS MD~t&=gUX7{N:c />IB9EJԕ ﱝPs9Hضe cݗv2`Yq'bTlJOJKԂ%b:&~J7bA:>QL_hѩ,菔AT{61q C Yc̝˔MQ;L "lBk@֎4ie>MǨIIH"I&F1Siάڷ,gY5%߿B,@ÐO1o/&nd6bάm[)!-0ȥؼTu8 [bћtwU{yuNǸhW:A"Np||xqznAxP'AHMki ;qƔZ콕i \w ϐ@3J__J_'_ 0Ip- x\Gmw#?SV|(BrVfG߰8\Em?sK}rI)k ذ'1s~ӳcsz|6n ^lW9.މvyl8Q>#g^wcP-3/1ֳ%Ơ pRg{2£ޝ9mH34*F/osf0< rb8x>HY¸ v'CIY$X0q|%7a<Um!< i^S@z}gOː^ 菔^eKbLd>[ňG=Wuq_[Q:8% <'H+1mݳKwCc 6x|}04`RxipMzY|B62> { wt-Uz f"w-ϝ&x4$6a8Hƒ?7ݽ8}jKJ(YSZ]AwSO³ ƮclxF;H[Xʗ~c) q*#W&\rښt:Eځޭ4qH$?õP{7;`s&(4M v:`)-{1`kR+Ʈe#>}?G]WA~>r}TGpؽlBn/ҭ[7Q kf$N6AnF 6&peC<=}Q=+&4KSH~? ɒiX 3ڻ)O>Y)䘣X8Ywc'5*-/89;™`6h# ߙ{ХBK^~KSv^AIPC<Cd;eńK}^N\\PCBFKЙ,0?5g CPhㅨ nA% @ Ań[ uKY @ r2z soR@j;'RϞ Iĉt6."&$t֭ikKL@ !@p GWA saLC0E̡ Jr@μ\N_:}C}r!(<O1˫R@ @ 215SHh-AB \=0 L.=pm:!=8۷c;gl![_?W)r>A1}6gmxONܞ)Hٌ Ucw1l6t^DhKTjt8yͩP'cOGK|4⚭qn/I(@VbqjbG106=/6P:yfwdʇ#fRis}1~Z?&}c=p&N[;l`>۪yl ݺuplfߛ h"GW/5m2R9cWK3W4:bX it +Hsle*tvr,'˱# @?!5fM/lT䥳_wb>qVz#&(hƿ)쮥w*Nٸm${o*25N& Ay Kwc-i;ЫwN nT_Pq8=a4)Qfw66dC6ztW!S;^bn '<* %x!M>EBK/fp>?DŽ[}Cd}z4X9~,ڶ\)g$g{嗣#o-C}Ts}Q م, 7 vWNF1֯IY; T+SIZ&p?N"cRl&o'sқ>= [`ٱlM^;"ٯv{; yL(j"R2/F-Bk=Fy^*Y߁QmfD#Fy2+Ylj:`Z̼{;e7~FW͘{F2P|讏ȯpa@ SK؛r6ʘm)ؕ7?#8>_Q4`:y$(;S#R[׃ȩql'?/3eV§j')LcZ젼/Ugmp.GWG9eP03Lo|NKSٹR̖D>0KNHmfczo0:9{BS3@FZ(A ~ԟ n4ݫ ma8two>Nszs".6n\}٢._~p|Fs<5AR\htF>!i~PC ׎(}qPBkiH/&n(LPB"R8ʰ"7Sn2%Q3ks&kXa ^?Џ#P;TOuP8f&=1["D" &b/{P`nl@WP4wIZw`2 tZP?2p9('獽o!(K'<< g[jbMo*$~P]IW``á`*NP{^'C F"?VH[K; x'&*'4>.as@ 7aBZ]NB|RRƪzx$(I~P!}cCy}L~z)` 1{3|k/V6.R~w7Fơ$8zG'-@})gKH}١yVk)d_Ur;87ƅ<|;w MdB{.ÖƎ ?BN"$PUnv|̨P ڽl?"&|Ў!Nv|aFzB@5il= f?m@ʨz2oo5B7 K wlvl6'Fb@ i-y>KV|ܚ: my+*^7`\FD ׍ P?pcZ ơ\?^)~ * 4L!]5y0AK+rK h @ =AR)RDҥmI*X H;0}EgzkL@y |o{P@p AA ]omm?MuCbBKLE סw@ 7W[.y@Вk*(~ixC{7Pv_"?A{[GItfcNә>csMcygd:/ca* 1wtf](vKI18>5<؉`ؓ#ecXi [2}& Z;_ͅrSrf=\c^ߐ33HR~ߜM٬1eX[yeT#|c' -cXRq'iobkzQw^Lw{I#7  y73ӷEs* Dyw"j0J#U3frh Q1͐ʚ5)LaoQI۱vMLyi&#bOK_̣B#~Գ >5ԜޗH9ơn`{>.ÐqY6[Mא]e˶1ǞH¿s8rÉ_rhF'\wWl/dPa2_qG Qњ(޹M)ÜEsѲs|(NK5c_)ٜN1:Z<`,y}9J WX֭`&Rb0d^JْU.d ުÁK ӣ#Pnᙿس™9c Ȓsixz-JP#A<Ue4û{\ n{CVY~,f vo֍ujC&9-B{ffw ;p) mdZ)kD,GHHʥX-Oԍ)|^ 8M}QJ_OZd+IJgKA<=:ä/SW:?ʴʌ5|}ć3-0VT '5s\[n[0sVO%$rζ8HؗJLRHʯ%lZVOS|%Ȫd@K$%l!ܵ*0ˡ;7of2w9lKSX%DKX[XʹlX3鼲9Fx~bk,˗F,$c9s4R3 3!B6/MEG9/5yugPM1g]|/9"?{fɆaO%xP*wp8*Q!WHE0k{l¥~k5P9ǴXl 鼷gsHޡOҊXG qA})7߾yi̱v蹌:'+6mM̔v:ּ!߹p5|xQ&O,j7N)I=u_dzb|j9"c'2bc`̮"z`2w7с2[y;]4鳂A;YhNsze)XGL;^BنpXA52 tDDiH?T aJe;zǪ1~Kȋ[u,qQSQ%j%QCՇUez9&ߢ_wLjIRT6T;.ҠN潧C@ pЉq]- qz7^,rL)kdvDDWeF2:€%l~.cgUgDf-gAG;@ (%-4 )CE@5Q!fF^UM/dSF_ ]Yퟴmz"h{k_['@e!;o-a dny~OƳ(MX:LXJ vb' al fE8ZϷ 5WGFfskp4{Y&Qa}MO[\*a^NP)mE'9#nJ!&@ fCW0b ",Tk0bعGHf;W'\@RN[8وawe@m]+F݂DŅzZM!+C&.`L+s;4յXc.%@1Z ]x?ZkkspW"XCm8+8Gr=+z[u$c4lc$(T}ƢV[&wci,Ť^McK7&TPFIUi>˥=MCaᨬ-Xv(C#b +YJM;ä@?pz2+4q 'LVEE -rk1J9]-*T{4Xl܃3 y8ed~k3-TsҲh{<__(|;܁NtSY)Zi9XJ=Gշ?{dTs񪻖Ac ^Eb@9zpj=[b. mÉ6C(>a[T7wƝRx睏 |tF @p &b_ y}vCKY hzl ٞNș9-`Aeb2HaGϜѧIڶByWf gTHIܶQ̙O)ڻ9Ϊվ8g C Yc̝˔ML?^PlR2*4 k>+gǺ&sX@VͪS91ɻ>̛o az4,wQ*WHܼh2)V% |6{+lz}1ՠc⣱h=.kTG Ch~c:=FA~̨%i3,0ϒǣQJ@},oo g>d葮u:[xDL9,TLwg0MD@ק~I|`|TRez>?hT2򖑗\A9|_fh8Z L#~W]ygNt2~DK$%b۸}jZyq$$`200\t$ie#ꋄ)Y ,$(t#^Ȋ?ʐY4)=As^|=d +e]_1ڥ:rm/CM8O *pglu/NT N7?<?%nA|N\zOw*ҽǭe#IwSO³ Ʈcl _LߧT|ʉ?"?/'j&el_uIx2|q[Kx ,$.]~j6vѲ{)?̥_ !cKCsipp8vl6VF``O"jv(ޘDwh0Xkk8\t@ nO TL&C*"HH$xyyEnMh)npA[@PpI7;Ap =d(o{ӷt!rM F{=V+AAA?tr@ "~C& (\׵>/{ @ uulm3<˙wM&Ew(W@ @ 6^/ @ @ /@ @ :@ @ t!(@ @ 0R@pe<9^$@ nXJKKE"NqS "+t^pt:q:v6 ݎf#((H$@ @"##E W@t:q88v{`ZZ"@ b@ @ F @ @ :Q @ _ JH$HRH$t֍nݺz٭[s< 돫5_ '\u kLWz(ݳ0:ᠺքތj*,+?M`Uײl cxx;1QҐj:I_+gqw~T>Be1=ڐA!- 3D>GHTѬ݊y`)v5< uMpw\5! aA~_kCϐ:ltTA~&>H=*RǸK ');bJn^l-4YVCXoH#@ps hmtç?ene]&*;dY_0ŷ8*%g-gɤ o-b!ad-C9CHVi^1p) $LI=k})Manh OEX[ˢ1$-b0eFd݂Mh3Krش=N?[x+,!5EaYxzp&= V<f]`1is{b/ERl'.lM?*3k!>;Á6<Ápty*%by\ꍻ8n5. De#c>[ {+3sx g<%+3$lgױPWLP/Elfa#;t3= y)9 D[Z➈h r"oW2;y!`: ";EVd2A˜PSK'zJZT@=̍s;88BZ{Y'+FfZ)Ip^UΌrM"1(ay$ ow ji׌+OWϽsoP) C5[gVWBA^㯁{ xN/Cu&7JрBFܡ>2r.8ZX͆vY HfeK؄P]XͷIjlЌ̟k}*8Ko_밁Y<_4Ic[T]K` Cl9g}<Ӭ'WqKXY Rm|WR|u,~qgOClxK'ӻKTwpP IDAT}Q}`Fwz _)-(Wu~=vYO1Y<eg5Fo.cն-]NLT [ >ϳ|5I?GxX.Ne ְ_))>qt%BB1 -G,hAqicqB-Ol\+oˊ);!gΊDxM?{ou?;QVG$ RHLPo6헖k]sN9rSj7%`0̇Ǡ" Z~<>uqi?L4'84ӆ++4Ha)Haݡ(G/#SdZrKF^W9܊[~'cJO K my$^] ,Tap+[i/Op"gKp`㇙>^#Q,3GqQP)p&Wאk A,3͡hiֱ ↟£WbU?2(BqM3?FbL!+r5W\Q+*p:DׇWRX=ɳmR.?q@0ab6Y1VgsR~q|a G ߐvuRFUSŬ*>\|*)mIs9q_KC9:؜ۉX?o"qOh-XNmΦ<jNcSJ=CQs4+z"ݛ^2ԺSm*[΀ Q{b*{Y.|muTPt8hj*SRXk $2&=\ƹ?rgD_쾠&&n/zɬTˤp=2KAgpO w" ? !(m֋Kܑ})w<@+F**F8!op:{g+Q]{e|2%m"cBUwm+pE)\Yg ;kc`uF$/ qM 8Gakf Y{\Xmq%D}W6Pў|?m9<"(Ņy!q\X0ӾL(tN l` ->=3|iߌ¹C38Kb F@&E1GݥŎfP^ µ/܍BFPiӽqJZ<,6 IZ\HBV=Yf݊1w2fo֦7W^ȽG*>] <xPu.Zo0W\L+ vƵ3Fn/iex`!ߟ)+eWz e wKOz C6q\JE+PX~qw|lq8D,G(܎Ktwۜwre@"߰a6~.zsPGm~!cˢ aο_]` xS?^>rJʡ|D %~DלTiNelЁQ,\(HEzֶy>6%C:(l4~rc7Hy[j=2{ ԝa]yGtW/an,ٸe%8YՊ`fK$(1mO>XXD;< ZcƲ2o}kZ!%̦.A6>0o*CTOqQ5Yd1g; A(KSY0v9ʌS.x$1}bagPOƸElO$n՜ T Ͳqh(!B#놈{aPp"\ )VrzB?Ǖ ʁ1+A5$ު26oo?P:fǣN&Ӓ+[o * ڡ7*[ZSi(#IIx/"#ecԘ ]PEg6h/ BSP@D&%NIuj|[ 2V:dlX/8(ʾ>%ԱL*P_M1I<ЍI]{&(<}k_1+wpr6\T0Imoca#9<ʭ;2X =2~:ۈy]&;f_~ʭ!CYg֫:ێq kv0vMҎ:_g 4h~k񠍐ߡS_eԫY1DyUB0|Cr/wc㮕g&-USz.MvRۭ w;ݾ?IS@zй-;n?`txo{.p+=Dfj7J }0V]æ\!ݠmbsчҩ%:>}z8vy@OxsO-Fe_#lg#n]sJk =C~ɜ]՗PܟXx?8Z-A/ׁP^Has V*rK<(ЩlA ~(ET4գ<^gv^mvJBj zwl X, k'P&T 2?OPb uF+~%I}BڥMrf1,F(`ˎhO".\LPpsrcH%iDR~|;'⪸7h/[=|lnT/G"uW0.!]NSq"-f;؞E8*!*$R왘PG5_䒭N;3쉗ރNR}_ n}מ"]_qۀH:"͂~堌ٜYYy_#KlWE O^.jJ5~4/O]WvyЗC`c&&R\ȥ΁ư/1i( ?zp'lv;ZId/6ޛϳK` ˟@neWG-3x~ o|iC=*r5rڋ#V,_ЦF峞chHC}kĝ0F Oe+M&obGpZ`|&֌؆N!H  wtppTT~E\%<ﰃ?cʁVgK^BpfS+i;bA НjճS*>+x1{o)ve/#(;=E[YWY7kGa< a \NDz\_$ bl\,wPh-'4 C 32,c_wP|Ѣ)Q Kd՚Dh͕bee-i܉Y2Nә<@AA%ud͖fNXxBtm BzC$ ԡn慎D\2P4 d^kT`+vvsn+&0nê1?8QIbżXn>l"O=}x楸{R{ED9$vɊ]ҝ N?=+&J = UXTwy(o]kMKw`_ܯY}f*W=?a;zE,vjޙٱ),b B%YK_sS=~:qWxڽm_+7;gDW&ǐqǵ X8]mC%B5ޯfGSX u_Є b\ o8\&rG㙔yGI@GUR!Wx2k7f2 yMVũ ~Cgs%!g__L 3D̛WS)k &8w6!AGc<*^ⲇ[Z CWd),  1塊m,p-% {K3Jk5&; 1PUT^@":Rp&_B- M&q e?[ArrSƟ2} ˩1ٯ< }o&LzWwyx@LFhUMg+.{ <>qk1OCϝ_"\,{'Bǻ;?畝d;V>mUmll8a+IhEb漀v}̺̇'Sp.!C@@R;wF.aW_ršLDH.[R8[_#H8tߧn}6h<;+qK@3E>mkDE蔻0786S&