pax_global_header00006660000000000000000000000064150252140140014504gustar00rootroot0000000000000052 comment=28beb292e957264d1947c438b2e4b0895247a18b icingadb-web-1.2.1/000077500000000000000000000000001502521401400140205ustar00rootroot00000000000000icingadb-web-1.2.1/AUTHORS000066400000000000000000000021001502521401400150610ustar00rootroot00000000000000Alexander A. Klimov 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.2.1/CHANGELOG.md000066400000000000000000000050401502521401400156300ustar00rootroot00000000000000# 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.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/2025/06/18/a-new-look-at-dependencies/ ## 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.2.1/CONTRIBUTING.md000066400000000000000000000142451502521401400162570ustar00rootroot00000000000000# 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.2.1/LICENSE000066400000000000000000000432541502521401400150350ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. 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. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. 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 convey 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision 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, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This 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. icingadb-web-1.2.1/README.md000066400000000000000000000057641502521401400153130ustar00rootroot00000000000000# Icinga DB Web [![PHP Support](https://img.shields.io/badge/php-%3E%3D%208.2-777BB4?logo=PHP)](https://php.net/) ![Build Status](https://github.com/Icinga/icingadb-web/actions/workflows/php.yml/badge.svg?branch=main) [![Github Tag](https://img.shields.io/github/tag/Icinga/icingadb-web.svg)](https://github.com/Icinga/icingadb-web/releases/latest) 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 2](LICENSE). icingadb-web-1.2.1/SECURITY.md000066400000000000000000000004401502521401400156070ustar00rootroot00000000000000# 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.2.1/application/000077500000000000000000000000001502521401400163235ustar00rootroot00000000000000icingadb-web-1.2.1/application/clicommands/000077500000000000000000000000001502521401400206145ustar00rootroot00000000000000icingadb-web-1.2.1/application/clicommands/MigrateCommand.php000066400000000000000000000753371502521401400242330ustar00rootroot00000000000000setLevel(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 && $configOwner !== $owner) { 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 && $configOwner !== $owner) { 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.2.1/application/controllers/000077500000000000000000000000001502521401400206715ustar00rootroot00000000000000icingadb-web-1.2.1/application/controllers/CommandTransportController.php000066400000000000000000000116131502521401400267430ustar00rootroot00000000000000assertPermission('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.2.1/application/controllers/CommentController.php000066400000000000000000000024651502521401400250570ustar00rootroot00000000000000addTitleTab(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.2.1/application/controllers/CommentsController.php000066400000000000000000000150171502521401400252370ustar00rootroot00000000000000addTitleTab(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); $continueWith = $this->createContinueWith(Links::commentsDetails(), $searchBar); $results = $comments->execute(); $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.2.1/application/controllers/ConfigController.php000066400000000000000000000027171502521401400246620ustar00rootroot00000000000000assertPermission('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.2.1/application/controllers/ContactController.php000066400000000000000000000023111502521401400250360ustar00rootroot00000000000000assertRouteAccess('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.2.1/application/controllers/ContactgroupController.php000066400000000000000000000024261502521401400261220ustar00rootroot00000000000000assertRouteAccess('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.2.1/application/controllers/ContactgroupsController.php000066400000000000000000000055171502521401400263110ustar00rootroot00000000000000assertRouteAccess(); } 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.2.1/application/controllers/ContactsController.php000066400000000000000000000055561502521401400252370ustar00rootroot00000000000000assertRouteAccess(); } 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.2.1/application/controllers/DowntimeController.php000066400000000000000000000032241502521401400252350ustar00rootroot00000000000000addTitleTab(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.2.1/application/controllers/DowntimesController.php000066400000000000000000000163531502521401400254270ustar00rootroot00000000000000addTitleTab(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); $continueWith = $this->createContinueWith(Links::downtimesDetails(), $searchBar); $results = $downtimes->execute(); $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.2.1/application/controllers/ErrorController.php000066400000000000000000000053511502521401400245430ustar00rootroot00000000000000document = 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.2.1/application/controllers/EventController.php000066400000000000000000000033611502521401400245320ustar00rootroot00000000000000addTitleTab(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.2.1/application/controllers/HealthController.php000066400000000000000000000110551502521401400246550ustar00rootroot00000000000000addTitleTab(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.2.1/application/controllers/HistoryController.php000066400000000000000000000105371502521401400251150ustar00rootroot00000000000000addTitleTab(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', '*') )); 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.2.1/application/controllers/HostController.php000066400000000000000000000462341502521401400243740ustar00rootroot00000000000000params->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); $history->peekAhead(); $page = $paginationControl->getCurrentPageNumber(); if ($page > 1 && ! $compact) { $history->resetOffset(); $history->limit($page * $limitControl->getLimit()); } $history->filter(Filter::lessThanOrEqual('event_time', $before)); yield $this->export($history); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $historyList = (new LoadMoreObjectList($history->execute())) ->setViewMode($viewModeSwitcher->getViewMode()) ->setPageSize($limitControl->getLimit()) ->setLoadMoreUrl($url->setParam('before', $before)); if ($compact) { $historyList->setPageNumber($page); } if ($compact && $page > 1) { $this->document->addFrom($historyList); } else { $this->addContent($historyList); } } 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'] ); yield $this->export($services); $serviceList = (new ObjectList($services)) ->setViewMode($viewModeSwitcher->getViewMode()) ->setEmptyStateMessage($paginationControl->getEmptyStateMessage()); $this->addControl($paginationControl); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $this->addContent($serviceList); $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 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')); } /** * 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::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' ]) ->setResultSetClass(VolatileStateResults::class); $this->joinFix($query, $this->host->id, $parents); $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)]; } /** * Filter the query to only include (direct) parents or children of the given object. * * @todo This is a workaround, remove it once https://github.com/Icinga/ipl-orm/issues/76 is fixed * * @param Query $query * @param string $objectId * @param bool $fetchParents Fetch parents if true, children otherwise */ protected function joinFix(Query $query, string $objectId, bool $fetchParents = false): void { $filterTable = $fetchParents ? 'child' : 'parent'; $utilizeType = $fetchParents ? 'parent' : 'child'; $edge = DependencyEdge::on($this->getDb()) ->utilize($utilizeType) ->columns([new Expression('1')]) ->filter(Filter::equal("$filterTable.host.id", $objectId)) ->filter(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->filter(new Exists( $edge->assembleSelect() ->where( "$edgeAlias.id = " . $query->getResolver()->qualifyColumn('id', $query->getModel()->getTableName()) ) )); } } icingadb-web-1.2.1/application/controllers/HostgroupController.php000066400000000000000000000125771502521401400254540ustar00rootroot00000000000000assertRouteAccess('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); $hostList = (new ObjectList($hosts)) ->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 ); $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.2.1/application/controllers/HostgroupsController.php000066400000000000000000000114321502521401400256240ustar00rootroot00000000000000assertRouteAccess(); } 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.2.1/application/controllers/HostsController.php000066400000000000000000000203051502521401400245460ustar00rootroot00000000000000addTitleTab(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); $continueWith = $this->createContinueWith(Links::hostsDetails(), $searchBar); $results = $hosts->execute(); 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.2.1/application/controllers/MigrateController.php000066400000000000000000000127641502521401400250500ustar00rootroot00000000000000assertHttpMethod('post'); if (! $this->getRequest()->isApiRequest()) { $this->httpBadRequest('No API request'); } if ( ! preg_match('/([^;]*);?/', $this->getRequest()->getHeader('Content-Type'), $matches) || $matches[1] !== 'application/json' ) { $this->httpBadRequest('No JSON content'); } $urls = $this->getRequest()->getPost(); $result = []; $errors = []; foreach ($urls as $urlString) { $url = Url::fromPath($urlString); if (UrlMigrator::isSupportedUrl($url)) { try { $urlString = rawurldecode(UrlMigrator::transformUrl($url)->getAbsoluteUrl()); } catch (Exception $e) { $errors[$urlString] = [ IcingaException::describe($e), IcingaException::getConfidentialTraceAsString($e) ]; $urlString = false; } } $result[] = $urlString; } $response = $this->getResponse()->json(); if (empty($errors)) { $response->setSuccessData($result); } else { $response->setFailData([ 'result' => $result, 'errors' => $errors ]); } $response->sendResponse(); } public function searchUrlAction() { $this->assertHttpMethod('post'); if (! $this->getRequest()->isApiRequest()) { $this->httpBadRequest('No API request'); } if ( ! preg_match('/([^;]*);?/', $this->getRequest()->getHeader('Content-Type'), $matches) || $matches[1] !== 'application/json' ) { $this->httpBadRequest('No JSON content'); } $urls = $this->getRequest()->getPost(); $result = []; foreach ($urls as $urlString) { $url = Url::fromPath($urlString); $params = $url->onlyWith(['sort', 'limit', 'view', 'columns', 'page'])->getParams(); $filter = $url->without(['sort', 'limit', 'view', 'columns', 'page'])->getParams(); $filter = QueryString::parse((string) $filter); $nonStrictOriginalFilter = QueryString::render($filter); $filter = UrlMigrator::transformLegacyWildcardFilter($filter); $nonStrictUpdatedFilter = QueryString::render($filter); if ($nonStrictUpdatedFilter !== $nonStrictOriginalFilter) { // The original filter might be formatted loosely, so if we render it again, // it might look different although nothing changed $result[] = rawurldecode($url->setParams($params)->setFilter($filter)->getAbsoluteUrl()); } else { $result[] = $urlString; } } $response = $this->getResponse()->json(); $response->setSuccessData($result); $response->sendResponse(); } public function checkboxStateAction() { $this->assertHttpMethod('get'); $form = new SetAsBackendForm(); $form->setAction(Url::fromPath('icingadb/migrate/checkbox-submit')->getAbsoluteUrl()); $this->getDocument()->addHtml($form); } public function checkboxSubmitAction() { $this->assertHttpMethod('post'); $this->addPart(HtmlString::create('"bogus"'), 'Behavior:Migrate'); (new SetAsBackendForm())->handleRequest(ServerRequest::fromGlobals()); } public function backendSupportAction() { $this->assertHttpMethod('post'); if (! $this->getRequest()->isApiRequest()) { $this->httpBadRequest('No API request'); } if ( ! preg_match('/([^;]*);?/', $this->getRequest()->getHeader('Content-Type'), $matches) || $matches[1] !== 'application/json' ) { $this->httpBadRequest('No JSON content'); } $moduleSupportStates = []; if ( Icinga::app()->getModuleManager()->hasEnabled('monitoring') && $this->Auth()->hasPermission('module/monitoring') ) { $supportList = []; foreach (Hook::all('Icingadb/IcingadbSupport') as $hook) { /** @var IcingadbSupportHook $hook */ $supportList[$hook->getModule()->getName()] = $hook->supportsIcingaDb(); } $moduleSupportStates = []; foreach ($this->getRequest()->getPost() as $moduleName) { if (isset($supportList[$moduleName])) { $moduleSupportStates[] = $supportList[$moduleName]; } else { $moduleSupportStates[] = false; } } } $this->getResponse() ->json() ->setSuccessData($moduleSupportStates) ->sendResponse(); } } icingadb-web-1.2.1/application/controllers/NotificationsController.php000066400000000000000000000104701502521401400262610ustar00rootroot00000000000000addTitleTab(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', '*') )); 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.2.1/application/controllers/RedundancygroupController.php000066400000000000000000000311641502521401400266240ustar00rootroot00000000000000params->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.2.1/application/controllers/ServiceController.php000066400000000000000000000417521502521401400250570ustar00rootroot00000000000000params->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); $history->peekAhead(); $page = $paginationControl->getCurrentPageNumber(); if ($page > 1 && ! $compact) { $history->resetOffset(); $history->limit($page * $limitControl->getLimit()); } $history->filter(Filter::lessThanOrEqual('event_time', $before)); yield $this->export($history); $this->addControl($sortControl); $this->addControl($limitControl); $this->addControl($viewModeSwitcher); $historyList = (new LoadMoreObjectList($history->execute())) ->setViewMode($viewModeSwitcher->getViewMode()) ->setPageSize($limitControl->getLimit()) ->setLoadMoreUrl($url->setParam('before', $before)); if ($compact) { $historyList->setPageNumber($page); } if ($compact && $page > 1) { $this->document->addFrom($historyList); } else { $this->addContent($historyList); } } 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 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')); } /** * 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.2.1/application/controllers/ServicegroupController.php000066400000000000000000000132431502521401400261260ustar00rootroot00000000000000assertRouteAccess('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); $serviceList = (new ObjectList($services)) ->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 ); $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.2.1/application/controllers/ServicegroupsController.php000066400000000000000000000106621502521401400263130ustar00rootroot00000000000000assertRouteAccess(); } 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.2.1/application/controllers/ServicesController.php000066400000000000000000000362661502521401400252460ustar00rootroot00000000000000addTitleTab(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); $continueWith = $this->createContinueWith(Links::servicesDetails(), $searchBar); $results = $services->execute(); 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); $this->params->shift('page'); // Handled by PivotTable internally $this->params->shift('limit'); // Handled by PivotTable internally $flipped = $this->params->shift('flipped', false); $problemToggle = $this->createProblemToggle(); $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); $continueWith = $this->createContinueWith(Links::servicesDetails(), $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; /** 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.2.1/application/controllers/TacticalController.php000066400000000000000000000055431502521401400252010ustar00rootroot00000000000000addTitleTab(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.2.1/application/controllers/UserController.php000066400000000000000000000012371502521401400243670ustar00rootroot00000000000000getRequest()->getUrl(); $url->setPath(preg_replace( '~^icingadb/user(?=/|$)~', 'icingadb/contact', $url->getPath() )); $this->getResponse() ->setHttpResponseCode(301) ->setHeader('Location', $url->getAbsoluteUrl()) ->sendResponse(); } } icingadb-web-1.2.1/application/controllers/UsergroupController.php000066400000000000000000000012631502521401400254430ustar00rootroot00000000000000getRequest()->getUrl(); $url->setPath(preg_replace( '~^icingadb/usergroup(?=/|$)~', 'icingadb/contactgroup', $url->getPath() )); $this->getResponse() ->setHttpResponseCode(301) ->setHeader('Location', $url->getAbsoluteUrl()) ->sendResponse(); } } icingadb-web-1.2.1/application/controllers/UsergroupsController.php000066400000000000000000000012671502521401400256320ustar00rootroot00000000000000getRequest()->getUrl(); $url->setPath(preg_replace( '~^icingadb/usergroups(?=/|$)~', 'icingadb/contactgroups', $url->getPath() )); $this->getResponse() ->setHttpResponseCode(301) ->setHeader('Location', $url->getAbsoluteUrl()) ->sendResponse(); } } icingadb-web-1.2.1/application/controllers/UsersController.php000066400000000000000000000012431502521401400245470ustar00rootroot00000000000000getRequest()->getUrl(); $url->setPath(preg_replace( '~^icingadb/users(?=/|$)~', 'icingadb/contacts', $url->getPath() )); $this->getResponse() ->setHttpResponseCode(301) ->setHeader('Location', $url->getAbsoluteUrl()) ->sendResponse(); } } icingadb-web-1.2.1/application/forms/000077500000000000000000000000001502521401400174515ustar00rootroot00000000000000icingadb-web-1.2.1/application/forms/ApiTransportForm.php000066400000000000000000000061041502521401400234350ustar00rootroot00000000000000addElement('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.2.1/application/forms/Command/000077500000000000000000000000001502521401400210275ustar00rootroot00000000000000icingadb-web-1.2.1/application/forms/Command/CommandForm.php000066400000000000000000000076331502521401400237530ustar00rootroot00000000000000 '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.2.1/application/forms/Command/Instance/000077500000000000000000000000001502521401400225735ustar00rootroot00000000000000icingadb-web-1.2.1/application/forms/Command/Instance/ToggleInstanceFeaturesForm.php000066400000000000000000000127351502521401400305450ustar00rootroot00000000000000featureStatus = $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.2.1/application/forms/Command/Object/000077500000000000000000000000001502521401400222355ustar00rootroot00000000000000icingadb-web-1.2.1/application/forms/Command/Object/AcknowledgeProblemForm.php000066400000000000000000000206031502521401400273370ustar00rootroot00000000000000on(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 while ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 250)); } } } icingadb-web-1.2.1/application/forms/Command/Object/AddCommentForm.php000066400000000000000000000137331502521401400256140ustar00rootroot00000000000000on(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 while ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 500)); } } } icingadb-web-1.2.1/application/forms/Command/Object/CheckNowForm.php000066400000000000000000000041331502521401400252740ustar00rootroot00000000000000 '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 while ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 1000)); } } } icingadb-web-1.2.1/application/forms/Command/Object/DeleteCommentForm.php000066400000000000000000000040721502521401400263220ustar00rootroot00000000000000 '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 while ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 500)); } } } icingadb-web-1.2.1/application/forms/Command/Object/DeleteDowntimeForm.php000066400000000000000000000050751502521401400265120ustar00rootroot00000000000000 '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 while ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 250)); } } } icingadb-web-1.2.1/application/forms/Command/Object/ProcessCheckResultForm.php000066400000000000000000000130421502521401400273450ustar00rootroot00000000000000on(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 while ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 250)); } } } icingadb-web-1.2.1/application/forms/Command/Object/RemoveAcknowledgementForm.php000066400000000000000000000051461502521401400300650ustar00rootroot00000000000000on(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 while ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 250)); } } } icingadb-web-1.2.1/application/forms/Command/Object/ScheduleCheckForm.php000066400000000000000000000112661502521401400262720ustar00rootroot00000000000000on(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 while ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 1000)); } } } icingadb-web-1.2.1/application/forms/Command/Object/ScheduleHostDowntimeForm.php000066400000000000000000000055111502521401400276750ustar00rootroot00000000000000start = 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.2.1/application/forms/Command/Object/ScheduleServiceDowntimeForm.php000066400000000000000000000255731502521401400303720ustar00rootroot00000000000000start = 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 while ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 250)); } } } icingadb-web-1.2.1/application/forms/Command/Object/SendCustomNotificationForm.php000066400000000000000000000111141502521401400302230ustar00rootroot00000000000000on(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 while ($granted->valid()) { // Chunk objects to avoid timeouts with large sets yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 500)); } } } icingadb-web-1.2.1/application/forms/Command/Object/ToggleObjectFeaturesForm.php000066400000000000000000000162221502521401400276440ustar00rootroot00000000000000 */ 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 while ($granted->valid()) { $this->submittedFeatures[$command->getFeature()] ??= $command->getEnabled(); // Chunk objects to avoid timeouts with large sets yield $command->setObjects(new LimitIterator(new NoRewindIterator($granted), 0, 1000)); } } } } icingadb-web-1.2.1/application/forms/DatabaseConfigForm.php000066400000000000000000000016061502521401400236430ustar00rootroot00000000000000setSubmitLabel(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.2.1/application/forms/Navigation/000077500000000000000000000000001502521401400215505ustar00rootroot00000000000000icingadb-web-1.2.1/application/forms/Navigation/ActionForm.php000066400000000000000000000027731502521401400243330ustar00rootroot00000000000000addElement( '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.2.1/application/forms/Navigation/IcingadbHostActionForm.php000066400000000000000000000003311502521401400265760ustar00rootroot00000000000000setSubmitLabel(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 (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.2.1/application/forms/SetAsBackendForm.php000066400000000000000000000015541502521401400233020ustar00rootroot00000000000000 'setAsBackendForm', 'class' => 'icinga-controls' ]; protected function assemble() { $this->addElement('checkbox', 'backend', [ 'class' => 'autosubmit', 'label' => t('Use Icinga DB As Backend'), 'value' => IcingadbSupportHook::isIcingaDbSetAsPreferredBackend() ]); } public function onSuccess() { Session::getSession()->getNamespace('icingadb')->set( IcingadbSupportHook::PREFERENCE_NAME, $this->getElement('backend')->isChecked() ); } } icingadb-web-1.2.1/application/views/000077500000000000000000000000001502521401400174605ustar00rootroot00000000000000icingadb-web-1.2.1/application/views/scripts/000077500000000000000000000000001502521401400211475ustar00rootroot00000000000000icingadb-web-1.2.1/application/views/scripts/joystickPagination-icingadb.phtml000066400000000000000000000122601502521401400276250ustar00rootroot00000000000000translate('%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.2.1/application/views/scripts/services/000077500000000000000000000000001502521401400227725ustar00rootroot00000000000000icingadb-web-1.2.1/application/views/scripts/services/grid-flipped.phtml000066400000000000000000000141001502521401400264020ustar00rootroot00000000000000compact): ?> 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.2.1/application/views/scripts/services/grid.phtml000066400000000000000000000141501502521401400247660ustar00rootroot00000000000000compact): ?> 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.2.1/configuration.php000066400000000000000000000535171502521401400174130ustar00rootroot00000000000000isWeb() && $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('usergroups', $routeDenylist) // TODO: Remove with 1.3, compat only && ! 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('users', $routeDenylist) // TODO: Remove with 1.3, compat only && ! 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('usergroups', $routeDenylist) // TODO: Remove with 1.3, compat only && ! 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('users', $routeDenylist) // TODO: Remove with 1.3, compat only && ! 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('migrate.js'); $this->provideJsFile('progress-bar.js'); } icingadb-web-1.2.1/doc/000077500000000000000000000000001502521401400145655ustar00rootroot00000000000000icingadb-web-1.2.1/doc/01-About.md000066400000000000000000000050061502521401400164000ustar00rootroot00000000000000# 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 2. icingadb-web-1.2.1/doc/02-Installation.md000066400000000000000000000022111502521401400177630ustar00rootroot00000000000000 # 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.2.1/doc/02-Installation.md.d/000077500000000000000000000000001502521401400202665ustar00rootroot00000000000000icingadb-web-1.2.1/doc/02-Installation.md.d/From-Source.md000066400000000000000000000014161502521401400227530ustar00rootroot00000000000000# 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.9) * [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (≥0.16.1) * [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) (≥0.13) icingadb-web-1.2.1/doc/03-Configuration.md000066400000000000000000000222301502521401400201350ustar00rootroot00000000000000# 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. ## 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.2.1/doc/04-Security.md000066400000000000000000000173511502521401400171460ustar00rootroot00000000000000# 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.2.1/doc/05-Upgrading.md000066400000000000000000000056031502521401400172550ustar00rootroot00000000000000# 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.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.2.1/doc/09-Automation.md000066400000000000000000000136661502521401400174710ustar00rootroot00000000000000# 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. 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" ``` ## 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 ### Acknowledge Problem #### Routes * icingadb/hosts/acknowledge * icingadb/services/acknowledge #### Options | Option | Required | Type | Depends On | |-------------|----------|----------|------------| | comment | y | Text | - | | persistent | n | BoolEnum | - | | notify | n | BoolEnum | - | | sticky | n | BoolEnum | - | | expire | n | BoolEnum | - | | expire_time | y | DateTime | expire | ### Add Comment #### Routes * icingadb/hosts/add-comment * icingadb/services/add-comment #### Options | Option | Required | Type | Depends On | |-------------|----------|----------|------------| | comment | y | Text | - | | expire | n | BoolEnum | - | | expire_time | y | DateTime | expire | ### 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 | |-------------|----------|----------| | check_time | y | DateTime | | force_check | n | BoolEnum | ### Schedule Host Downtime #### Routes * icingadb/hosts/schedule-downtime #### Options | Option | Required | Type | Depends On | |---------------|----------|-------------|------------| | comment | y | Text | - | | start | y | DateTime | - | | end | y | DateTime | - | | flexible | n | BoolEnum | - | | hours | y | Number | flexible | | minutes | y | Number | flexible | | all_services | n | BoolEnum | - | | child_options | n | ChildOption | - | ### Schedule Service Downtime #### Routes * icingadb/services/schedule-check #### Options | Option | Required | Type | Depends On | |----------|----------|-------------|------------| | comment | y | Text | - | | start | y | DateTime | - | | end | y | DateTime | - | | flexible | n | BoolEnum | - | | hours | y | Number | flexible | | minutes | y | Number | flexible | ### 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 | |---------|----------|----------| | comment | y | Text | | forced | n | BoolEnum | icingadb-web-1.2.1/doc/10-Migration.md000066400000000000000000000203131502521401400172550ustar00rootroot00000000000000# 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. ### Automation For those who integrate Icinga Web into e.g. custom dashboards, there is also a way to automate the migration of urls. An API endpoint in Icinga DB Web allows for this: `/icingaweb2/icingadb/migrate/monitoring-url` If you `POST` a JSON list there, you'll get a JSON list back with the transformed urls in it. The returned list is ordered the same and any unrecognized url is left unchanged: **Input:** ```json [ "/icingaweb2/monitoring/list/services?hostgroup_name=prod-hosts|(_host_env=prod&_host_stage!=testing)", "/icingaweb2/businessprocess/process/show?config=production" ] ``` **Output**: ```json [ "/icingaweb2/icingadb/services?hostgroup.name=prod-hosts|(host.vars.env=prod&host.vars.stage!=testing)", "/icingaweb2/businessprocess/process/show?config=production" ] ``` **cURL example:** `curl -s -HContent-Type:application/json -HAccept:application/json -u icingaadmin:icinga http://localhost/icingaweb2/icingadb/migrate/monitoring-url -d '["/icingaweb2/monitoring/list/services?hostgroup_name=prod-hosts|(_host_env=prod&_host_stage!=testing)","/icingaweb2/businessprocess/process/show?config=production"]'` ## 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.2.1/doc/11-Concepts.md000066400000000000000000000112001502521401400170760ustar00rootroot00000000000000# 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.2.1/doc/TRADEMARKS.md000066400000000000000000000013451502521401400165470ustar00rootroot00000000000000# 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.2.1/doc/res/000077500000000000000000000000001502521401400153565ustar00rootroot00000000000000icingadb-web-1.2.1/doc/res/CheckStatisticsAnatomy.jpg000066400000000000000000002353451502521401400225150ustar00rootroot00000000000000JFIFHH@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.2.1/doc/res/ListAnatomyOverdue.jpg000066400000000000000000005405341502521401400216710ustar00rootroot00000000000000JFIFHH@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.2.1/library/000077500000000000000000000000001502521401400154645ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/000077500000000000000000000000001502521401400171645ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Authentication/000077500000000000000000000000001502521401400221435ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Authentication/ObjectAuthorization.php000066400000000000000000000211151502521401400266430ustar00rootroot00000000000000getTableName(); $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.2.1/library/Icingadb/Command/000077500000000000000000000000001502521401400205425ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Command/IcingaApiCommand.php000066400000000000000000000045111502521401400243770ustar00rootroot00000000000000setEndpoint($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.2.1/library/Icingadb/Command/IcingaCommand.php000066400000000000000000000007171502521401400237510ustar00rootroot00000000000000feature = $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.2.1/library/Icingadb/Command/Object/000077500000000000000000000000001502521401400217505ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Command/Object/AcknowledgeProblemCommand.php000066400000000000000000000055041502521401400275300ustar00rootroot00000000000000sticky = $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.2.1/library/Icingadb/Command/Object/AddCommentCommand.php000066400000000000000000000014361502521401400257770ustar00rootroot00000000000000expireTime = $expireTime; return $this; } /** * Get the time when the acknowledgement should expire * * @return ?int */ public function getExpireTime() { return $this->expireTime; } } icingadb-web-1.2.1/library/Icingadb/Command/Object/CommandAuthor.php000066400000000000000000000014401502521401400252210ustar00rootroot00000000000000author = $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.2.1/library/Icingadb/Command/Object/DeleteCommentCommand.php000066400000000000000000000003461502521401400265100ustar00rootroot00000000000000attributes; } /** * Set the attributes to query * * @param array $attributes * * @return $this */ public function setAttributes(array $attributes): self { $this->attributes = $attributes; return $this; } } icingadb-web-1.2.1/library/Icingadb/Command/Object/ObjectsCommand.php000066400000000000000000000032031502521401400253470ustar00rootroot00000000000000 */ protected $objects; /** * Set the involved objects * * @param Traversable $objects Except generators * * @return $this * * @throws InvalidArgumentException If a generator is passed */ public function setObjects(Traversable $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 Traversable */ public function getObjects(): Traversable { if ($this->objects === null) { throw new LogicException( 'You are accessing an unset property. Please make sure to set it beforehand.' ); } return $this->objects; } } icingadb-web-1.2.1/library/Icingadb/Command/Object/ProcessCheckResultCommand.php000066400000000000000000000051111502521401400275310ustar00rootroot00000000000000status = $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|null $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.2.1/library/Icingadb/Command/Object/PropagateHostDowntimeCommand.php000066400000000000000000000021041502521401400302440ustar00rootroot00000000000000setChildOption( $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.2.1/library/Icingadb/Command/Object/RemoveAcknowledgementCommand.php000066400000000000000000000004051502521401400302440ustar00rootroot00000000000000checkTime = $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.2.1/library/Icingadb/Command/Object/ScheduleDowntimeCommand.php000066400000000000000000000140601502521401400272240ustar00rootroot00000000000000start = $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.2.1/library/Icingadb/Command/Object/ScheduleHostDowntimeCommand.php000066400000000000000000000004331502521401400300610ustar00rootroot00000000000000forced; } /** * 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.2.1/library/Icingadb/Command/Object/ToggleObjectFeatureCommand.php000066400000000000000000000050251502521401400276460ustar00rootroot00000000000000feature = $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.2.1/library/Icingadb/Command/Object/WithCommentCommand.php000066400000000000000000000016151502521401400262210ustar00rootroot00000000000000comment = $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.2.1/library/Icingadb/Command/Renderer/000077500000000000000000000000001502521401400223105ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Command/Renderer/IcingaApiCommandRenderer.php000066400000000000000000000257731502521401400276510ustar00rootroot00000000000000app; } /** * 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.2.1/library/Icingadb/Command/Renderer/IcingaCommandRendererInterface.php000066400000000000000000000003151502521401400310210ustar00rootroot00000000000000renderer = 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() ); } 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 $command * @param int|null $now * * @throws CommandTransportException * * @return mixed */ public function send(IcingaCommand $command, int $now = null) { return $this->sendCommand($this->renderer->render($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.2.1/library/Icingadb/Command/Transport/CommandTransport.php000066400000000000000000000077051502521401400265530ustar00rootroot00000000000000isEmpty()) { 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|null $now Timestamp of the command or null for now * * @throws CommandTransportException If sending the Icinga command failed * * @return mixed */ public function send(IcingaCommand $command, int $now = null) { $errors = []; foreach (static::getConfig() as $name => $transportConfig) { $transport = static::createTransport($transportConfig); try { $result = $transport->send($command, $now); } catch (CommandTransportException $e) { Logger::error($e); $errors[] = sprintf('%s: %s.', $name, rtrim($e->getMessage(), '.')); continue; // Try the next transport } 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.2.1/library/Icingadb/Command/Transport/CommandTransportConfig.php000066400000000000000000000011661502521401400276740ustar00rootroot00000000000000 [ 'name' => 'commandtransports', 'module' => 'icingadb', 'keyColumn' => 'name' ] ]; protected $queryColumns = [ 'transport' => [ 'name', 'transport', // API options 'host', 'port', 'username', 'password' ] ]; } icingadb-web-1.2.1/library/Icingadb/Command/Transport/CommandTransportException.php000066400000000000000000000004111502521401400304150ustar00rootroot00000000000000getAuth()->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)) { if ($name === 'contacts' && array_key_exists('users', $routeDenylist)) { return false; // TODO: Remove with 1.3, compat only } elseif ($name === 'contactgroups' && array_key_exists('usergroups', $routeDenylist)) { return false; // TODO: Remove with 1.3, compat only } 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); } /** * 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(); } else { $queries = [$query]; } $orgQuery = $query; 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 = in_array('service', $relations, true); $applyHostRestriction = 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; $hostStateRelation = array_search('host_state', $relations, true); $serviceStateRelation = array_search('service_state', $relations, true); $resolver = $query->getResolver(); $queryFilter = Filter::any(); $obfuscationRules = Filter::any(); foreach ($this->getAuth()->getUser()->getRoles() as $role) { $roleFilter = Filter::all(); if ($customVarRelationName !== false) { if (($restriction = $role->getRestrictions('icingadb/denylist/variables'))) { $roleFilter->add($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')) { $roleFilter->add(Filter::any( Filter::all( Filter::unlike('host.id', '*'), Filter::unlike('service.id', '*') ), $this->parseRestriction($restriction, 'icingadb/filter/objects') )); } if ($applyHostRestriction && ($restriction = $role->getRestrictions('icingadb/filter/hosts'))) { $hostFilter = $this->parseRestriction($restriction, 'icingadb/filter/hosts'); if ($orgQuery instanceof UnionQuery) { $this->forceQueryOptimization($hostFilter, 'hostgroup.name'); } $roleFilter->add(Filter::any(Filter::unlike('host.id', '*'), $hostFilter)); } if ( $applyServiceRestriction && ($restriction = $role->getRestrictions('icingadb/filter/services')) ) { $serviceFilter = $this->parseRestriction($restriction, 'icingadb/filter/services'); if ($orgQuery instanceof UnionQuery) { $this->forceQueryOptimization($serviceFilter, 'servicegroup.name'); } $roleFilter->add(Filter::any(Filter::unlike('service.id', '*'), $serviceFilter)); } } if (! $roleFilter->isEmpty()) { $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); } } $query->filter($queryFilter); } } /** * 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())) { return; } } elseif ($column === $condition->getColumn()) { 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; } /** * 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.2.1/library/Icingadb/Common/Backend.php000066400000000000000000000142521502521401400224600ustar00rootroot00000000000000get('icingadb', 'resource') )); $config->options = [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ]; if ($config->db === 'mysql') { $config->options[PDO::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 = str_replace(' user ', sprintf(' %s ', $quoted), $sql); $sql = str_replace(' user.', sprintf(' %s.', $quoted), $sql); $sql = str_replace('(user.', 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.2.1/library/Icingadb/Common/BaseFilter.php000066400000000000000000000004021502521401400231410ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Common/CommandActions.php000066400000000000000000000240721502521401400240310ustar00rootroot00000000000000 $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.2.1/library/Icingadb/Common/Database.php000066400000000000000000000005101502521401400226250ustar00rootroot00000000000000detailActionsDisabled = $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.2.1/library/Icingadb/Common/HostLink.php000066400000000000000000000012421502521401400226570ustar00rootroot00000000000000state->getStateText(), StateBall::SIZE_MEDIUM); $content[] = ' '; } $content[] = $host->display_name; return Html::tag('a', ['href' => Links::host($host), 'class' => 'subject'], $content); } } icingadb-web-1.2.1/library/Icingadb/Common/HostLinks.php000066400000000000000000000042641502521401400230510ustar00rootroot00000000000000 $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.2.1/library/Icingadb/Common/HostStates.php000066400000000000000000000052741502521401400232360ustar00rootroot00000000000000redisUnavailable && $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|null $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|null $moduleConfig * @param Config|null $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|null $moduleConfig * @param Config|null $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.2.1/library/Icingadb/Common/Icons.php000066400000000000000000000010201502521401400221710ustar00rootroot00000000000000 $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.2.1/library/Icingadb/Common/LoadMore.php000066400000000000000000000046261502521401400226370ustar00rootroot00000000000000pageSize = $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.2.1/library/Icingadb/Common/Macros.php000066400000000000000000000102611502521401400223510ustar00rootroot00000000000000 $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.2.1/library/Icingadb/Common/ObjectInspectionDetail.php000066400000000000000000000263731502521401400255250ustar00rootroot00000000000000 ['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.2.1/library/Icingadb/Common/SearchControls.php000066400000000000000000000042141502521401400240570ustar00rootroot00000000000000getModel(), $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 * * @return ContinueWith */ public function createContinueWith(Url $detailsUrl, SearchBar $searchBar): ContinueWith { $continueWith = new ContinueWith($detailsUrl, [$searchBar, 'getFilter']); $continueWith->setTitle(t('Show bulk processing actions for all filtered results')); $continueWith->setBaseTarget('_next'); $continueWith->getAttributes() ->set('id', $this->getRequest()->protectId('continue-with')); $searchBar->getWrapper()->add($continueWith); return $continueWith; } } icingadb-web-1.2.1/library/Icingadb/Common/ServiceLink.php000066400000000000000000000022151502521401400233430ustar00rootroot00000000000000state->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.2.1/library/Icingadb/Common/ServiceLinks.php000066400000000000000000000061261502521401400235330ustar00rootroot00000000000000 $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.2.1/library/Icingadb/Common/ServiceStates.php000066400000000000000000000065651502521401400237250ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Common/TicketLinks.php000066400000000000000000000022321502521401400233500ustar00rootroot00000000000000ticketLinkDisabled = $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.2.1/library/Icingadb/Compat/000077500000000000000000000000001502521401400204075ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Compat/CompatHost.php000066400000000000000000000116621502521401400232070ustar00rootroot00000000000000 ['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.2.1/library/Icingadb/Compat/CompatObject.php000066400000000000000000000236521502521401400235020ustar00rootroot00000000000000object = $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.2.1/library/Icingadb/Compat/CompatService.php000066400000000000000000000161441502521401400236720ustar00rootroot00000000000000 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.2.1/library/Icingadb/Compat/UrlMigrator.php000066400000000000000000001453601502521401400234000ustar00rootroot00000000000000 ['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|null $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.2.1/library/Icingadb/Data/000077500000000000000000000000001502521401400200355ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Data/CsvResultSet.php000066400000000000000000000002771502521401400231620ustar00rootroot00000000000000 */ 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; } do { $query->offset($offset); $result = $query->execute()->disableCache(); foreach ($result as $i => $keysAndValues) { if ($i === 0) { echo implode(',', array_keys($keysAndValues)); } echo "\r\n"; echo implode(',', array_values($keysAndValues)); JsonResultSet::giveMeMoreTime(); } $offset += 1000; } while ($result->hasMore()); exit; } } icingadb-web-1.2.1/library/Icingadb/Data/DependencyNodes.php000066400000000000000000000036501502521401400236210ustar00rootroot00000000000000filter = $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.2.1/library/Icingadb/Data/JsonResultSet.php000066400000000000000000000003011502521401400233240ustar00rootroot00000000000000 */ 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) { $keysAndValues[$key] = $this->createObject($value); } 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; } echo '['; do { $query->offset($offset); $result = $query->execute()->disableCache(); foreach ($result as $i => $object) { if ($i > 0 || $offset !== 0) { 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.2.1/library/Icingadb/Data/PivotTable.php000066400000000000000000000314011502521401400226160ustar00rootroot00000000000000 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.2.1/library/Icingadb/Data/VolatileCsvResults.php000066400000000000000000000005241502521401400243640ustar00rootroot00000000000000 '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; } // 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.2.1/library/Icingadb/Hook/Common/000077500000000000000000000000001502521401400213145ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Hook/Common/HookUtils.php000066400000000000000000000014441502521401400237510ustar00rootroot00000000000000init(); } /** * 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.2.1/library/Icingadb/Hook/Common/TotalSlaReportUtils.php000066400000000000000000000030311502521401400257620ustar00rootroot00000000000000getData($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.2.1/library/Icingadb/Hook/CustomVarRendererHook.php000066400000000000000000000056731502521401400250430ustar00rootroot00000000000000prefetchForObject($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.2.1/library/Icingadb/Hook/EventDetailExtensionHook.php000066400000000000000000000010401502521401400255120ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/Hook/ExtensionHook/ObjectDetailExtensionHook.php000066400000000000000000000115231502521401400304430ustar00rootroot00000000000000 * * @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.2.1/library/Icingadb/Hook/ExtensionHook/ObjectsDetailExtensionHook.php000066400000000000000000000075051502521401400306330ustar00rootroot00000000000000 * * @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.2.1/library/Icingadb/Hook/HostActionsHook.php000066400000000000000000000007671502521401400236660ustar00rootroot00000000000000limit(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.2.1/library/Icingadb/Hook/IcingadbSupportHook.php000066400000000000000000000026341502521401400245200ustar00rootroot00000000000000getNamespace('icingadb') ->get(self::PREFERENCE_NAME, false); } /** * 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.2.1/library/Icingadb/Hook/PluginOutputHook.php000066400000000000000000000036141502521401400241010ustar00rootroot00000000000000isSupportedCommand($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.2.1/library/Icingadb/Hook/RedundancyGroupDetailExtensionHook.php000066400000000000000000000011411502521401400275440ustar00rootroot00000000000000limit(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.2.1/library/Icingadb/Hook/TabHook.php000066400000000000000000000031641502521401400221300ustar00rootroot00000000000000loadTabHooks(); 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.2.1/library/Icingadb/Hook/UserDetailExtensionHook.php000066400000000000000000000010231502521401400253500ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/ActionUrl.php000066400000000000000000000027241502521401400226420ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/Behavior/000077500000000000000000000000001502521401400217635ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Model/Behavior/ActionAndNoteUrl.php000066400000000000000000000022261502521401400256470ustar00rootroot00000000000000 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) { $column = $condition->metaData()->get('columnName'); if (! isset($this->properties[$column])) { return; } $values = $condition->getValue(); if (! is_array($values)) { if (is_int($values) || ctype_digit($values)) { return; } $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)); } } icingadb-web-1.2.1/library/Icingadb/Model/Behavior/BoolCast.php000066400000000000000000000004211502521401400241770ustar00rootroot00000000000000 'contact', 'usergroup' => 'contactgroup' ]; /** @var Query */ protected $query; public function setQuery(Query $query) { $this->query = $query; return $this; } public function rewriteCondition(Filter\Condition $condition, $relation = null) { $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; } } public function rewriteColumn($column, $relation = null) { $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.2.1/library/Icingadb/Model/Behavior/HasProblematicParent.php000066400000000000000000000056271502521401400265550ustar00rootroot00000000000000query = $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) { $column = substr($condition->getColumn(), strlen($relation ?? '')); if ($this->isSelectableColumn($column)) { throw new InvalidColumnException($column, $this->query->getModel()); } } } icingadb-web-1.2.1/library/Icingadb/Model/Behavior/ReRoute.php000066400000000000000000000046421502521401400240670ustar00rootroot00000000000000routes = $routes; } public function getRoutes(): array { return $this->routes; } public function rewriteCondition(Filter\Condition $condition, $relation = null) { $remainingPath = $condition->metaData()->get('columnName', ''); if (strpos($remainingPath, '.') === false) { return; } 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; } } 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.2.1/library/Icingadb/Model/Checkcommand.php000066400000000000000000000053441502521401400233170ustar00rootroot00000000000000 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); $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.2.1/library/Icingadb/Model/CheckcommandArgument.php000066400000000000000000000052601502521401400250170ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/CheckcommandCustomvar.php000066400000000000000000000025101502521401400252130ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/CheckcommandEnvvar.php000066400000000000000000000032271502521401400244770ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/Comment.php000066400000000000000000000071231502521401400223420ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/CommentHistory.php000066400000000000000000000071601502521401400237250ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/Customvar.php000066400000000000000000000045431502521401400227260ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/CustomvarFlat.php000066400000000000000000000151271502521401400235350ustar00rootroot00000000000000add(new Binary([ 'id', 'environment_id', 'customvar_id', 'flatname_checksum' ])); $behaviors->add(new class implements RewriteFilterBehavior { public function rewriteCondition(Condition $condition, $relation = null) { 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); } } }); } public function createRelations(Relations $relations) { $relations->belongsTo('environment', Environment::class); $relations->belongsTo('customvar', Customvar::class); $relations->belongsToMany('checkcommand', Checkcommand::class) ->through(CheckcommandCustomvar::class) ->setCandidateKey('customvar_id'); $relations->belongsToMany('eventcommand', Eventcommand::class) ->through(EventcommandCustomvar::class) ->setCandidateKey('customvar_id'); $relations->belongsToMany('host', Host::class) ->through(HostCustomvar::class) ->setCandidateKey('customvar_id'); $relations->belongsToMany('hostgroup', Hostgroup::class) ->through(HostgroupCustomvar::class) ->setCandidateKey('customvar_id'); $relations->belongsToMany('notification', Notification::class) ->through(NotificationCustomvar::class) ->setCandidateKey('customvar_id'); $relations->belongsToMany('notificationcommand', Notificationcommand::class) ->through(NotificationcommandCustomvar::class) ->setCandidateKey('customvar_id'); $relations->belongsToMany('service', Service::class) ->through(ServiceCustomvar::class) ->setCandidateKey('customvar_id'); $relations->belongsToMany('servicegroup', Servicegroup::class) ->through(ServicegroupCustomvar::class) ->setCandidateKey('customvar_id'); $relations->belongsToMany('timeperiod', Timeperiod::class) ->through(TimeperiodCustomvar::class) ->setCandidateKey('customvar_id'); $relations->belongsToMany('user', User::class) ->through(UserCustomvar::class) ->setCandidateKey('customvar_id'); $relations->belongsToMany('usergroup', Usergroup::class) ->through(UsergroupCustomvar::class) ->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.2.1/library/Icingadb/Model/DependencyEdge.php000066400000000000000000000037511502521401400236060ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/DependencyEdgeState.php000066400000000000000000000020031502521401400245740ustar00rootroot00000000000000add(new Binary([ 'id', 'environment_id' ])); $behaviors->add(new BoolCast([ 'failed' ])); } public function createRelations(Relations $relations) { $relations->hasMany('edge', DependencyEdge::class); } } icingadb-web-1.2.1/library/Icingadb/Model/DependencyNode.php000066400000000000000000000103701502521401400236220ustar00rootroot00000000000000 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'); } } icingadb-web-1.2.1/library/Icingadb/Model/Downtime.php000066400000000000000000000125171502521401400225310ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/DowntimeHistory.php000066400000000000000000000115761502521401400241170ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/Endpoint.php000066400000000000000000000036301502521401400225170ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/Environment.php000066400000000000000000000116111502521401400232410ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/Eventcommand.php000066400000000000000000000053441502521401400233630ustar00rootroot00000000000000 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); $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.2.1/library/Icingadb/Model/EventcommandArgument.php000066400000000000000000000052601502521401400250630ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/EventcommandCustomvar.php000066400000000000000000000025101502521401400252570ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/EventcommandEnvvar.php000066400000000000000000000032361502521401400245430ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/FlappingHistory.php000066400000000000000000000063101502521401400240570ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/History.php000066400000000000000000000105501502521401400223770ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/Host.php000066400000000000000000000252551502521401400216630ustar00rootroot00000000000000 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) { if (! $subject->customvar instanceof ResultSet) { $this->applyRestrictions($subject->customvar); } $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->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); $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('history', History::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.2.1/library/Icingadb/Model/HostCustomvar.php000066400000000000000000000024201502521401400235540ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/HostState.php000066400000000000000000000067221502521401400226620ustar00rootroot00000000000000 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::text($this->soft_state); } } icingadb-web-1.2.1/library/Icingadb/Model/Hostgroup.php000066400000000000000000000054471502521401400227410ustar00rootroot00000000000000 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); $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.2.1/library/Icingadb/Model/HostgroupCustomvar.php000066400000000000000000000024631502521401400246400ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/HostgroupMember.php000066400000000000000000000023741502521401400240650ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/Hostgroupsummary.php000066400000000000000000000221651502521401400243530ustar00rootroot00000000000000on($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() { $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.2.1/library/Icingadb/Model/HoststateSummary.php000066400000000000000000000062541502521401400243000ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/IconImage.php000066400000000000000000000022641502521401400225740ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/Instance.php000066400000000000000000000053351502521401400225070ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/LastHostComment.php000066400000000000000000000007651502521401400240310ustar00rootroot00000000000000belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->belongsTo('state', HostState::class) ->setForeignKey('last_comment_id') ->setCandidateKey('id'); } } icingadb-web-1.2.1/library/Icingadb/Model/LastServiceComment.php000066400000000000000000000007731502521401400245130ustar00rootroot00000000000000belongsTo('environment', Environment::class); $relations->belongsTo('zone', Zone::class); $relations->belongsTo('state', ServiceState::class) ->setForeignKey('last_comment_id') ->setCandidateKey('id'); } } icingadb-web-1.2.1/library/Icingadb/Model/NotesUrl.php000066400000000000000000000027141502521401400225140ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/Notification.php000066400000000000000000000113441502521401400233660ustar00rootroot00000000000000 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); $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.2.1/library/Icingadb/Model/NotificationCustomvar.php000066400000000000000000000025101502521401400252650ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/NotificationHistory.php000066400000000000000000000074271502521401400247570ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/NotificationUser.php000066400000000000000000000022041502521401400242200ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/NotificationUsergroup.php000066400000000000000000000022471502521401400253040ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/Notificationcommand.php000066400000000000000000000056341502521401400247320ustar00rootroot00000000000000 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); $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.2.1/library/Icingadb/Model/NotificationcommandArgument.php000066400000000000000000000054721502521401400264350ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/NotificationcommandCustomvar.php000066400000000000000000000025711502521401400266330ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/NotificationcommandEnvvar.php000066400000000000000000000033601502521401400261060ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/RedundancyGroup.php000066400000000000000000000046661502521401400240620ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/RedundancyGroupState.php000066400000000000000000000046521502521401400250560ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/RedundancyGroupSummary.php000066400000000000000000000153601502521401400254310ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/Schema.php000066400000000000000000000016561502521401400221450ustar00rootroot00000000000000add(new Binary(['id'])); $behaviors->add(new MillisecondTimestamp(['timestamp'])); } } icingadb-web-1.2.1/library/Icingadb/Model/Service.php000066400000000000000000000250361502521401400223430ustar00rootroot00000000000000 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) { if (! $subject->customvar instanceof ResultSet) { $this->applyRestrictions($subject->customvar); } $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->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); $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.2.1/library/Icingadb/Model/ServiceCustomvar.php000066400000000000000000000024451502521401400242460ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/ServiceState.php000066400000000000000000000071501502521401400233410ustar00rootroot00000000000000 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::text($this->soft_state); } } icingadb-web-1.2.1/library/Icingadb/Model/Servicegroup.php000066400000000000000000000054041502521401400234150ustar00rootroot00000000000000 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); $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.2.1/library/Icingadb/Model/ServicegroupCustomvar.php000066400000000000000000000025101502521401400253140ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/ServicegroupMember.php000066400000000000000000000022271502521401400245450ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/ServicegroupSummary.php000066400000000000000000000151501502521401400247720ustar00rootroot00000000000000on($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() { $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.2.1/library/Icingadb/Model/ServicestateSummary.php000066400000000000000000000110731502521401400247560ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/SlaHistoryDowntime.php000066400000000000000000000035201502521401400245450ustar00rootroot00000000000000add(new MillisecondTimestamp([ 'downtime_start', 'downtime_end' ])); $behaviors->add(new Binary([ 'environment_id', 'endpoint_id', 'host_id', 'service_id', 'downtime_id' ])); } 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.2.1/library/Icingadb/Model/SlaHistoryState.php000066400000000000000000000033031502521401400240360ustar00rootroot00000000000000add(new MillisecondTimestamp([ 'event_time' ])); $behaviors->add(new Binary([ 'environment_id', 'endpoint_id', 'host_id', 'service_id', 'downtime_id' ])); } 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.2.1/library/Icingadb/Model/State.php000066400000000000000000000145731502521401400220270ustar00rootroot00000000000000add(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); 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); 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.2.1/library/Icingadb/Model/StateHistory.php000066400000000000000000000072121502521401400234010ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/Timeperiod.php000066400000000000000000000057211502521401400230430ustar00rootroot00000000000000 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); $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.2.1/library/Icingadb/Model/TimeperiodCustomvar.php000066400000000000000000000024721502521401400247470ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/TimeperiodOverrideExclude.php000066400000000000000000000024311502521401400260500ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/TimeperiodOverrideInclude.php000066400000000000000000000024311502521401400260420ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/TimeperiodRange.php000066400000000000000000000026641502521401400240230ustar00rootroot00000000000000 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.2.1/library/Icingadb/Model/UnreachableParent.php000066400000000000000000000165571502521401400243360ustar00rootroot00000000000000belongsTo('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->setAccessible(true); $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.2.1/library/Icingadb/Model/UnreachableParent/000077500000000000000000000000001502521401400236075ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Model/UnreachableParent/ResultSet.php000066400000000000000000000007651502521401400262620ustar00rootroot00000000000000id])) { continue; } $knownIds[$value->id] = true; yield $value; } } } icingadb-web-1.2.1/library/Icingadb/Model/User.php000066400000000000000000000111301502521401400216470ustar00rootroot00000000000000 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); $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.2.1/library/Icingadb/Model/UserCustomvar.php000066400000000000000000000024201502521401400235550ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/Usergroup.php000066400000000000000000000056441502521401400227410ustar00rootroot00000000000000 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); $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.2.1/library/Icingadb/Model/UsergroupCustomvar.php000066400000000000000000000024631502521401400246410ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/UsergroupMember.php000066400000000000000000000021631502521401400240620ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Model/Vars.php000066400000000000000000000013611502521401400216510ustar00rootroot00000000000000add(new FlattenedObjectVars()); } } icingadb-web-1.2.1/library/Icingadb/Model/Zone.php000066400000000000000000000051451502521401400216550ustar00rootroot00000000000000 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.2.1/library/Icingadb/ProvidedHook/000077500000000000000000000000001502521401400215615ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/ProvidedHook/ApplicationState.php000066400000000000000000000072541502521401400255460ustar00rootroot00000000000000getNamespace('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.2.1/library/Icingadb/ProvidedHook/CreateHostSlaReport.php000066400000000000000000000017551502521401400261770ustar00rootroot00000000000000hasPermission('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.2.1/library/Icingadb/ProvidedHook/CreateHostsSlaReport.php000066400000000000000000000022361502521401400263550ustar00rootroot00000000000000hasPermission('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.2.1/library/Icingadb/ProvidedHook/CreateServiceSlaReport.php000066400000000000000000000021541502521401400266540ustar00rootroot00000000000000hasPermission('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.2.1/library/Icingadb/ProvidedHook/CreateServicesSlaReport.php000066400000000000000000000022571502521401400270430ustar00rootroot00000000000000hasPermission('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.2.1/library/Icingadb/ProvidedHook/IcingaHealth.php000066400000000000000000000131571502521401400246210ustar00rootroot00000000000000getInstance(); 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.2.1/library/Icingadb/ProvidedHook/RedisHealth.php000066400000000000000000000042161502521401400244710ustar00rootroot00000000000000getConnection()->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.2.1/library/Icingadb/ProvidedHook/Reporting/000077500000000000000000000000001502521401400235325ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/ProvidedHook/Reporting/HostSlaReport.php000066400000000000000000000032361502521401400270200ustar00rootroot00000000000000getModuleManager()->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.2.1/library/Icingadb/ProvidedHook/Reporting/ServiceSlaReport.php000066400000000000000000000035721502521401400275060ustar00rootroot00000000000000getModuleManager()->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.2.1/library/Icingadb/ProvidedHook/Reporting/SlaReport.php000066400000000000000000000223111502521401400261550ustar00rootroot00000000000000createReportData(); $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); 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.2.1/library/Icingadb/ProvidedHook/Reporting/TotalHostSlaReport.php000066400000000000000000000005531502521401400300230ustar00rootroot00000000000000getDb()->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.2.1/library/Icingadb/Redis/000077500000000000000000000000001502521401400202325ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Redis/VolatileStateResults.php000066400000000000000000000136551502521401400251170ustar00rootroot00000000000000resolver = $query->getResolver(); $self->redisUnavailable = Backend::getRedis()->isUnavailable(); return $self; } /** * Get whether Redis is unavailable * * @return bool */ public function isRedisUnavailable(): bool { return $this->redisUnavailable; } #[\ReturnTypeWillChange] public function current() { if (! $this->redisUnavailable && ! $this->updatesApplied && ! $this->isCacheDisabled) { $this->rewind(); } return parent::current(); } public function next(): void { parent::next(); if (! $this->redisUnavailable && $this->isCacheDisabled && $this->valid()) { $this->applyRedisUpdates([parent::current()]); } } 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): array { return [$state->getColumns(), $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); 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); $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.2.1/library/Icingadb/Setup/000077500000000000000000000000001502521401400202645ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Setup/ApiTransportPage.php000066400000000000000000000077351502521401400242340ustar00rootroot00000000000000setName('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.2.1/library/Icingadb/Setup/ApiTransportStep.php000066400000000000000000000056731502521401400242720ustar00rootroot00000000000000data = $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.2.1/library/Icingadb/Setup/DbResourcePage.php000066400000000000000000000121071502521401400236300ustar00rootroot00000000000000setName('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.2.1/library/Icingadb/Setup/DbResourceStep.php000066400000000000000000000122201502521401400236630ustar00rootroot00000000000000data = $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.2.1/library/Icingadb/Setup/IcingaDbWizard.php000066400000000000000000000060351502521401400236220ustar00rootroot00000000000000addPage(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.2.1/library/Icingadb/Setup/RedisPage.php000066400000000000000000000036431502521401400226460ustar00rootroot00000000000000setName('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.2.1/library/Icingadb/Setup/RedisStep.php000066400000000000000000000160711502521401400227040ustar00rootroot00000000000000data = $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.2.1/library/Icingadb/Setup/WelcomePage.php000066400000000000000000000027461502521401400231760ustar00rootroot00000000000000setName('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.2.1/library/Icingadb/Util/000077500000000000000000000000001502521401400201015ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Util/FeatureStatus.php000066400000000000000000000030771502521401400234200ustar00rootroot00000000000000 $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.2.1/library/Icingadb/Util/ObjectSuggestionsCursor.php000066400000000000000000000014371502521401400254560ustar00rootroot00000000000000 $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.2.1/library/Icingadb/Util/PerfData.php000066400000000000000000000425701502521401400223100ustar00rootroot00000000000000perfdataValue = $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.2.1/library/Icingadb/Util/PerfDataFormat.php000066400000000000000000000106601502521401400234540ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Util/PerfDataSet.php000066400000000000000000000110171502521401400227540ustar00rootroot00000000000000perfdataStr = $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.2.1/library/Icingadb/Util/PluginOutput.php000066400000000000000000000127661502521401400233050ustar00rootroot00000000000000', '', '', '', '', '', '@@@@@@' ]; /** @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.2.1/library/Icingadb/Util/ThresholdRange.php000066400000000000000000000110061502521401400235210ustar00rootroot00000000000000 * * @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.2.1/library/Icingadb/View/000077500000000000000000000000001502521401400200765ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/View/BaseHostAndServiceRenderer.php000066400000000000000000000250241502521401400257550ustar00rootroot00000000000000 */ 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($item->state->getIcon()); $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($item->state->getIcon()); 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_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('random', ['title' => $title])); } if (! $item->notifications_enabled) { $statusIcons->addHtml( new Icon('bell-slash', ['title' => $this->translate('Notifications disabled')]) ); } if (! $item->active_checks_enabled) { $statusIcons->addHtml( new Icon('eye-slash', ['title' => $this->translate('Active checks disabled')]) ); } $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; } } icingadb-web-1.2.1/library/Icingadb/View/CommentRenderer.php000066400000000000000000000117151502521401400237050ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/DowntimeRenderer.php000066400000000000000000000167011502521401400240710ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/EventRenderer.php000066400000000000000000000401651502521401400233650ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/HostRenderer.php000066400000000000000000000017051502521401400232160ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/HostgroupGridRenderer.php000066400000000000000000000136121502521401400251010ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/HostgroupRenderer.php000066400000000000000000000067511502521401400243010ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/NotificationRenderer.php000066400000000000000000000155131502521401400247310ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/RedundancyGroupRenderer.php000066400000000000000000000054051502521401400254130ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/ServiceRenderer.php000066400000000000000000000026441502521401400237040ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/ServicegroupGridRenderer.php000066400000000000000000000245101502521401400255630ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/ServicegroupRenderer.php000066400000000000000000000062001502521401400247510ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/UserRenderer.php000066400000000000000000000036121502521401400232160ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/View/UsergroupRenderer.php000066400000000000000000000036511502521401400242760ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/Web/000077500000000000000000000000001502521401400177015ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Web/Control/000077500000000000000000000000001502521401400213215ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Web/Control/GridViewModeSwitcher.php000066400000000000000000000017351502521401400260760ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Web/Control/ProblemToggle.php000066400000000000000000000030011502521401400245660ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Web/Control/SearchBar/000077500000000000000000000000001502521401400231535ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Web/Control/SearchBar/ObjectSuggestions.php000066400000000000000000000441261502521401400273340ustar00rootroot00000000000000customVarSources = [ '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( 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; foreach (self::collectFilterColumns($model, $query->getResolver()) 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); $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; } } icingadb-web-1.2.1/library/Icingadb/Web/Control/ViewModeSwitcher.php000066400000000000000000000124311502521401400252630ustar00rootroot00000000000000 'view-mode-switcher', 'name' => 'view-mode-switcher' ]; /** @var string Default view mode */ const DEFAULT_VIEW_MODE = 'common'; /** @var string Default view mode param */ 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()); if (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.2.1/library/Icingadb/Web/Controller.php000066400000000000000000000463571502521401400225540ustar00rootroot00000000000000filter === 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; } } $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([ //TODO: Don't set store key as it will no longer be needed once we drop support for // lower version of icingaweb2 then v2.11. //https://github.com/Icinga/icingaweb2/pull/4765 'store' => Config::app()->get('global', 'config_backend', 'db'), '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) . '" */'; } $this->content->add(Html::tag('pre', $sql)); } 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(); } /** * @todo Remove once support for Icinga Web 2 v2.9.x is dropped */ protected function sendAsPdf() { if (! Icinga::app()->getModuleManager()->has('pdfexport')) { throw new ConfigurationError('The pdfexport module is required for exports to PDF'); } if (version_compare(Version::VERSION, '2.10.0', '>=')) { parent::sendAsPdf(); return; } putenv('ICINGAWEB_EXPORT_FORMAT=pdf'); Environment::raiseMemoryLimit('512M'); Environment::raiseExecutionTime(300); $time = DateFormatter::formatDateTime(time()); $doc = (new PrintableHtmlDocument()) ->setTitle($this->view->title ?? '') ->setHeader(Html::wantHtml([ Html::tag('span', ['class' => 'title']), Html::tag('time', null, $time) ])) ->setFooter(Html::wantHtml([ Html::tag('span', null, [ t('Page') . ' ', Html::tag('span', ['class' => 'pageNumber']), ' / ', Html::tag('span', ['class' => 'totalPages']) ]), Html::tag('p', null, Url::fromRequest()->setParams($this->params)) ])) ->addHtml($this->content); $doc->getAttributes()->add('class', 'icinga-module module-icingadb'); Pdfexport::first()->streamPdfFromHtml($doc, sprintf( '%s-%s', $this->view->title ?: $this->getRequest()->getActionName(), $time )); } 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.2.1/library/Icingadb/Web/Navigation/000077500000000000000000000000001502521401400220005ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Web/Navigation/Action.php000066400000000000000000000053101502521401400237250ustar00rootroot00000000000000object = $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.2.1/library/Icingadb/Web/Navigation/IcingadbHostAction.php000066400000000000000000000002311502521401400262010ustar00rootroot00000000000000getDb()); $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.2.1/library/Icingadb/Web/Navigation/Renderer/ProblemsBadge.php000066400000000000000000000074601502521401400267740ustar00rootroot00000000000000count === 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.2.1/library/Icingadb/Web/Navigation/Renderer/ServiceProblemsBadge.php000066400000000000000000000017301502521401400303070ustar00rootroot00000000000000getDb()); $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.2.1/library/Icingadb/Web/Navigation/Renderer/TotalProblemsBadge.php000066400000000000000000000033271502521401400277760ustar00rootroot00000000000000 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.2.1/library/Icingadb/Widget/000077500000000000000000000000001502521401400204075ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Widget/CheckAttempt.php000066400000000000000000000026071502521401400235010ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/DependencyNodeStateBadges.php000066400000000000000000000014341502521401400261150ustar00rootroot00000000000000addAttributes(['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.2.1/library/Icingadb/Widget/DependencyNodeStatistics.php000066400000000000000000000032031502521401400260550ustar00rootroot00000000000000summary = $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.2.1/library/Icingadb/Widget/Detail/000077500000000000000000000000001502521401400216115ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Widget/Detail/CheckStatistics.php000066400000000000000000000337501502521401400254220ustar00rootroot00000000000000 SVG; 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.2.1/library/Icingadb/Widget/Detail/CommentDetail.php000066400000000000000000000106151502521401400250520ustar00rootroot00000000000000 ['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('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.2.1/library/Icingadb/Widget/Detail/CustomVarTable.php000066400000000000000000000151011502521401400252130ustar00rootroot00000000000000 ['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.2.1/library/Icingadb/Widget/Detail/DowntimeCard.php000066400000000000000000000220211502521401400246770ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Detail/DowntimeDetail.php000066400000000000000000000165271502521401400252460ustar00rootroot00000000000000 ['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('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.2.1/library/Icingadb/Widget/Detail/EventDetail.php000066400000000000000000000721471502521401400245410ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Detail/HostDetail.php000066400000000000000000000036771502521401400243770ustar00rootroot00000000000000serviceSummary = $serviceSummary; } protected function createServiceStatistics(): array { if ($this->serviceSummary->services_total > 0) { $services = new ServiceStatistics($this->serviceSummary); $services->setBaseFilter(Filter::equal('host.name', $this->object->name)); } 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.2.1/library/Icingadb/Widget/Detail/HostInspectionDetail.php000066400000000000000000000010031502521401400264100ustar00rootroot00000000000000add([ $this->createSourceLocation(), $this->createLastCheckResult(), $this->createAttributes(), $this->createCustomVariables(), $this->createRedisInfo() ]); } } icingadb-web-1.2.1/library/Icingadb/Widget/Detail/HostMetaInfo.php000066400000000000000000000044651502521401400246730ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Detail/HostStatistics.php000066400000000000000000000033671502521401400253230ustar00rootroot00000000000000summary = $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.2.1/library/Icingadb/Widget/Detail/MultiselectQuickActions.php000066400000000000000000000175241502521401400271430ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Detail/ObjectDetail.php000066400000000000000000000647411502521401400246670ustar00rootroot00000000000000 ['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.2.1/library/Icingadb/Widget/Detail/ObjectHeader.php000066400000000000000000000071371502521401400246510ustar00rootroot00000000000000object = $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.2.1/library/Icingadb/Widget/Detail/ObjectStatistics.php000066400000000000000000000027221502521401400256060ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Detail/ObjectsDetail.php000066400000000000000000000144411502521401400250420ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Detail/PerfDataTable.php000066400000000000000000000100431502521401400247560ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Detail/QuickActions.php000066400000000000000000000116671502521401400247320ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Detail/RedundancyGroupDetail.php000066400000000000000000000106161502521401400265620ustar00rootroot00000000000000 ['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', ]) ->setResultSetClass(VolatileStateResults::class) ->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.2.1/library/Icingadb/Widget/Detail/ServiceDetail.php000066400000000000000000000023421502521401400250460ustar00rootroot00000000000000add($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.2.1/library/Icingadb/Widget/Detail/ServiceInspectionDetail.php000066400000000000000000000010061502521401400270760ustar00rootroot00000000000000add([ $this->createSourceLocation(), $this->createLastCheckResult(), $this->createAttributes(), $this->createCustomVariables(), $this->createRedisInfo() ]); } } icingadb-web-1.2.1/library/Icingadb/Widget/Detail/ServiceMetaInfo.php000066400000000000000000000037311502521401400253510ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Detail/ServiceStatistics.php000066400000000000000000000042561502521401400260040ustar00rootroot00000000000000summary = $summary; } 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 = Links::services(); 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; } } icingadb-web-1.2.1/library/Icingadb/Widget/Detail/UserDetail.php000066400000000000000000000145451502521401400243740ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Detail/UsergroupDetail.php000066400000000000000000000056361502521401400254520ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Health.php000066400000000000000000000053701502521401400223320ustar00rootroot00000000000000data = $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.2.1/library/Icingadb/Widget/HostStateBadges.php000066400000000000000000000017121502521401400241450ustar00rootroot00000000000000addAttributes(['class' => 'host-state-badges']); $this->add(array_filter([ $this->createGroup('down'), $this->createBadge('unknown'), $this->createBadge('up'), $this->createBadge('pending') ])); } } icingadb-web-1.2.1/library/Icingadb/Widget/HostStatusBar.php000066400000000000000000000011131502521401400236620ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Widget/HostSummaryDonut.php000066400000000000000000000051731502521401400244330ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/IconImage.php000066400000000000000000000034561502521401400227630ustar00rootroot00000000000000source = $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.2.1/library/Icingadb/Widget/ItemList/000077500000000000000000000000001502521401400221415ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Widget/ItemList/CommandTransportList.php000066400000000000000000000012101502521401400267730ustar00rootroot00000000000000getAttributes()->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.2.1/library/Icingadb/Widget/ItemList/CommandTransportListItem.php000066400000000000000000000043141502521401400276220ustar00rootroot00000000000000list->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.2.1/library/Icingadb/Widget/ItemList/LoadMoreObjectList.php000066400000000000000000000022211502521401400263340ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/Widget/ItemList/ObjectList.php000066400000000000000000000216231502521401400247200ustar00rootroot00000000000000 */ 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('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('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.2.1/library/Icingadb/Widget/ItemList/PageSeparatorItem.php000066400000000000000000000013751502521401400262340ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/ItemList/TicketLinkObjectList.php000066400000000000000000000020121502521401400266710ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/Widget/ItemTable/000077500000000000000000000000001502521401400222555ustar00rootroot00000000000000icingadb-web-1.2.1/library/Icingadb/Widget/ItemTable/BaseStateRowItem.php000066400000000000000000000055611502521401400261570ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/ItemTable/HostItemTable.php000066400000000000000000000012561502521401400254760ustar00rootroot00000000000000initializeDetailActions(); $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.2.1/library/Icingadb/Widget/ItemTable/HostRowItem.php000066400000000000000000000027141502521401400252160ustar00rootroot00000000000000list->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.2.1/library/Icingadb/Widget/ItemTable/ObjectGrid.php000066400000000000000000000076651502521401400250200ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/ItemTable/ObjectTable.php000066400000000000000000000033751502521401400251540ustar00rootroot00000000000000 */ 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.2.1/library/Icingadb/Widget/ItemTable/ServiceItemTable.php000066400000000000000000000012751502521401400261620ustar00rootroot00000000000000initializeDetailActions(); $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.2.1/library/Icingadb/Widget/ItemTable/ServiceRowItem.php000066400000000000000000000034411502521401400256770ustar00rootroot00000000000000list->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.2.1/library/Icingadb/Widget/ItemTable/StateItemTable.php000066400000000000000000000156011502521401400256400ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/ItemTable/StateRowItem.php000066400000000000000000000120771502521401400253640ustar00rootroot00000000000000item->state->getStateText(), StateBall::SIZE_LARGE); $stateBall->add($this->item->state->getIcon()); $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.2.1/library/Icingadb/Widget/MarkdownLine.php000066400000000000000000000011561502521401400235150ustar00rootroot00000000000000 ['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.2.1/library/Icingadb/Widget/MarkdownText.php000066400000000000000000000011421502521401400235450ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/Notice.php000066400000000000000000000013771502521401400223510ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/PluginOutputContainer.php000066400000000000000000000010641502521401400254430ustar00rootroot00000000000000setHtmlContent($output); $this->getAttributes()->registerAttributeCallback('class', function () use ($output) { return $output->isHtml() ? 'plugin-output' : 'plugin-output preformatted'; }); } } icingadb-web-1.2.1/library/Icingadb/Widget/ServiceStateBadges.php000066400000000000000000000020161502521401400246260ustar00rootroot00000000000000addAttributes(['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.2.1/library/Icingadb/Widget/ServiceStatusBar.php000066400000000000000000000011771502521401400243570ustar00rootroot00000000000000add(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.2.1/library/Icingadb/Widget/ServiceSummaryDonut.php000066400000000000000000000061121502521401400251100ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/ShowMore.php000066400000000000000000000024241502521401400226650ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/StateBadge.php000066400000000000000000000003361502521401400231250ustar00rootroot00000000000000 '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.2.1/library/Icingadb/Widget/TagList.php000066400000000000000000000012411502521401400224650ustar00rootroot00000000000000 'tag-list']; 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.2.1/module.info000066400000000000000000000003421502521401400161610ustar00rootroot00000000000000Module: icingadb Version: 1.2.0 Requires: Libraries: icinga-php-library (>=0.16.1), icinga-php-thirdparty (>=0.13.0) Description: Icinga DB Web UI for Icinga DB – Provides a graphical interface to your Icinga monitoring icingadb-web-1.2.1/phpstan-baseline-7x.neon000066400000000000000000000072661502521401400205050ustar00rootroot00000000000000parameters: ignoreErrors: - message: "#^Parameter \\#1 \\$data of function hex2bin expects string, mixed given\\.$#" count: 1 path: application/controllers/EventController.php - message: "#^Parameter \\#1 \\$str of function md5 expects string, mixed given\\.$#" count: 1 path: application/forms/RedisConfigForm.php - message: "#^Parameter \\#1 \\$stack of function array_pop expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Command/Transport/ApiCommandTransport.php - message: "#^Parameter \\#2 \\.\\.\\.\\$args of function array_merge expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: "#^Parameter \\#1 \\$arr1 of function array_diff_key expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: "#^Parameter \\#1 \\$data of function bin2hex expects string, mixed given\\.$#" count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: "#^Parameter \\#1 \\$str of function strtolower expects string, mixed given\\.$#" count: 2 path: library/Icingadb/Compat/UrlMigrator.php - message: "#^Parameter \\#1 \\$input of function array_keys expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Data/CsvResultSet.php - message: "#^Parameter \\#1 \\$input of function array_values expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Data/CsvResultSet.php - message: "#^Parameter \\#2 \\$str of function explode expects string, mixed given\\.$#" count: 1 path: library/Icingadb/Data/PivotTable.php - message: "#^Parameter \\#1 \\$input of function array_keys expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Data/VolatileCsvResults.php - message: "#^Parameter \\#1 \\$input of function array_values expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Data/VolatileCsvResults.php - message: "#^Parameter \\#2 \\$str of function explode expects string, mixed given\\.$#" count: 1 path: library/Icingadb/Model/Behavior/ActionAndNoteUrl.php - message: "#^Parameter \\#2 \\.\\.\\.\\$args of function array_merge expects array, array\\\\|false given\\.$#" count: 1 path: library/Icingadb/Model/CustomvarFlat.php - message: "#^Parameter \\#1 \\$var of function count expects array\\|Countable, Icinga\\\\Module\\\\Reporting\\\\ReportData\\|null given\\.$#" count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: "#^Parameter \\#1 \\$number of function number_format expects float, float\\|int\\|string given\\.$#" count: 1 path: library/Icingadb/Util/PerfData.php - message: "#^Parameter \\#1 \\$str of function trim expects string, string\\|null given\\.$#" count: 1 path: library/Icingadb/Util/PluginOutput.php - message: "#^Parameter \\#3 \\$encoding of function htmlspecialchars expects string, null given\\.$#" count: 1 path: library/Icingadb/Util/PluginOutput.php - message: "#^Parameter \\#1 \\$str of function trim expects string, mixed given\\.$#" count: 1 path: library/Icingadb/Web/Controller.php - message: "#^Parameter \\#2 \\$str of function explode expects string, mixed given\\.$#" count: 1 path: library/Icingadb/Web/Controller.php - message: "#^Parameter \\#1 \\$objects of method Icinga\\\\Module\\\\Icingadb\\\\Forms\\\\Command\\\\CommandForm\\:\\:setObjects\\(\\) expects array\\\\|\\(Countable&Traversable\\\\), array\\\\|false given\\.$#" count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php icingadb-web-1.2.1/phpstan-baseline-8x.neon000066400000000000000000000070421502521401400204760ustar00rootroot00000000000000parameters: ignoreErrors: - message: "#^Parameter \\#1 \\$string of function hex2bin expects string, mixed given\\.$#" count: 1 path: application/controllers/EventController.php - message: "#^Parameter \\#1 \\$string of function md5 expects string, mixed given\\.$#" count: 1 path: application/forms/RedisConfigForm.php - message: "#^Parameter \\#1 \\$array of function array_pop expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Command/Transport/ApiCommandTransport.php - message: "#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: "#^Parameter \\#1 \\$array of function array_diff_key expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: "#^Parameter \\#1 \\$string of function bin2hex expects string, mixed given\\.$#" count: 1 path: library/Icingadb/Common/ObjectInspectionDetail.php - message: "#^Parameter \\#1 \\$string of function strtolower expects string, mixed given\\.$#" count: 2 path: library/Icingadb/Compat/UrlMigrator.php - message: "#^Parameter \\#1 \\$array of function array_keys expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Data/CsvResultSet.php - message: "#^Parameter \\#1 \\$array of function array_values expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Data/CsvResultSet.php - message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#" count: 1 path: library/Icingadb/Data/PivotTable.php - message: "#^Parameter \\#1 \\$array of function array_keys expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Data/VolatileCsvResults.php - message: "#^Parameter \\#1 \\$array of function array_values expects array, mixed given\\.$#" count: 1 path: library/Icingadb/Data/VolatileCsvResults.php - message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#" count: 1 path: library/Icingadb/Model/Behavior/ActionAndNoteUrl.php - message: "#^Parameter \\#2 \\.\\.\\.\\$arrays of function array_merge expects array, array\\\\|false given\\.$#" count: 1 path: library/Icingadb/Model/CustomvarFlat.php - message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, Icinga\\\\Module\\\\Reporting\\\\ReportData\\|null given\\.$#" count: 1 path: library/Icingadb/ProvidedHook/Reporting/SlaReport.php - message: "#^Parameter \\#1 \\$num of function number_format expects float, float\\|int\\|string given\\.$#" count: 1 path: library/Icingadb/Util/PerfData.php - message: "#^Parameter \\#1 \\$string of function trim expects string, string\\|null given\\.$#" count: 1 path: library/Icingadb/Util/PluginOutput.php - message: "#^Parameter \\#1 \\$string of function trim expects string, mixed given\\.$#" count: 1 path: library/Icingadb/Web/Controller.php - message: "#^Parameter \\#2 \\$string of function explode expects string, mixed given\\.$#" count: 1 path: library/Icingadb/Web/Controller.php - message: "#^Parameter \\#1 \\$objects of method Icinga\\\\Module\\\\Icingadb\\\\Forms\\\\Command\\\\CommandForm\\:\\:setObjects\\(\\) expects array\\\\|\\(Countable&Traversable\\\\), array\\ given\\.$#" count: 1 path: library/Icingadb/Widget/Detail/MultiselectQuickActions.php icingadb-web-1.2.1/phpstan-baseline-by-php-version.php000066400000000000000000000003341502521401400226460ustar00rootroot00000000000000= 80000) { $includes[] = __DIR__ . '/phpstan-baseline-8x.neon'; } else { $includes[] = __DIR__ . '/phpstan-baseline-7x.neon'; } return [ 'includes' => $includes ]; icingadb-web-1.2.1/phpstan-baseline-standard.neon000066400000000000000000012025331502521401400217420ustar00rootroot00000000000000parameters: 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\:\:acknowledgeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:addCommentAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:assertIsGrantedOnCommandTargets\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:checkNowAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:fetchCommandTargets\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: application/controllers/CommentController.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: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:processCheckresultAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:removeAcknowledgementAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:scheduleCheckAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:scheduleDowntimeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:sendCustomNotificationAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:toggleFeaturesAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/CommentController.php - message: '#^Parameter \#1 \$form of method Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\: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/CommentController.php - message: '#^Parameter \#2 \$value of static method ipl\\Stdlib\\Filter\:\:equal\(\) expects array\|bool\|float\|int\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/CommentController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\CommentController\:\:\$commandTargetModel \(ipl\\Orm\\Model\) in isset\(\) is not nullable\.$#' identifier: isset.property 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\:\:acknowledgeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimeController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:addCommentAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimeController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:assertIsGrantedOnCommandTargets\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimeController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:checkNowAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimeController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:fetchCommandTargets\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: application/controllers/DowntimeController.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: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:processCheckresultAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimeController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:removeAcknowledgementAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimeController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:scheduleCheckAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimeController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:scheduleDowntimeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimeController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:sendCustomNotificationAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimeController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:toggleFeaturesAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/DowntimeController.php - message: '#^Parameter \#1 \$form of method Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\: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/DowntimeController.php - message: '#^Parameter \#2 \$value of static method ipl\\Stdlib\\Filter\:\:equal\(\) expects array\|bool\|float\|int\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/DowntimeController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\DowntimeController\:\:\$commandTargetModel \(ipl\\Orm\\Model\) in isset\(\) is not nullable\.$#' identifier: isset.property 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: '#^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\:\:fetchCommandTargets\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue 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\:\:equal\(\) expects array\|bool\|float\|int\|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\\MigrateController\:\:backendSupportAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/MigrateController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\MigrateController\:\:checkboxStateAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/MigrateController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\MigrateController\:\:checkboxSubmitAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/MigrateController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\MigrateController\:\:monitoringUrlAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/MigrateController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\MigrateController\:\:searchUrlAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/MigrateController.php - message: '#^Parameter \#1 \$url of static method Icinga\\Web\\Url\:\:fromPath\(\) expects string, mixed given\.$#' identifier: argument.type count: 2 path: application/controllers/MigrateController.php - message: '#^Parameter \#2 \$subject of function preg_match expects string, string\|false given\.$#' identifier: argument.type count: 3 path: application/controllers/MigrateController.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\:\:fetchCommandTargets\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue 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\:\:equal\(\) expects array\|bool\|float\|int\|string, mixed given\.$#' identifier: argument.type count: 2 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\\Controllers\\UserController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/UserController.php - message: '#^Parameter \#1 \$title of method ipl\\Web\\Compat\\CompatController\:\:setTitle\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/UserController.php - message: '#^Parameter \#2 \$value of static method ipl\\Stdlib\\Filter\:\:equal\(\) expects array\|bool\|float\|int\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/UserController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\UserController\:\:\$user \(Icinga\\Module\\Icingadb\\Model\\User\) does not accept ipl\\Orm\\Model\.$#' identifier: assign.propertyType count: 1 path: application/controllers/UserController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\UsergroupController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/UsergroupController.php - message: '#^Parameter \#1 \$title of method ipl\\Web\\Compat\\CompatController\:\:setTitle\(\) expects string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/UsergroupController.php - message: '#^Parameter \#2 \$value of static method ipl\\Stdlib\\Filter\:\:equal\(\) expects array\|bool\|float\|int\|string, mixed given\.$#' identifier: argument.type count: 1 path: application/controllers/UsergroupController.php - message: '#^Property Icinga\\Module\\Icingadb\\Controllers\\UsergroupController\:\:\$usergroup \(Icinga\\Module\\Icingadb\\Model\\Usergroup\) does not accept ipl\\Orm\\Model\.$#' identifier: assign.propertyType count: 1 path: application/controllers/UsergroupController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\UsergroupsController\:\:completeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/UsergroupsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\UsergroupsController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/UsergroupsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\UsergroupsController\:\:searchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/UsergroupsController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\UsersController\:\:completeAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/UsersController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\UsersController\:\:indexAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/UsersController.php - message: '#^Method Icinga\\Module\\Icingadb\\Controllers\\UsersController\:\:searchEditorAction\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/controllers/UsersController.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 \#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\\Html\\Contract\\FormElement\:\:isChecked\(\)\.$#' identifier: method.notFound count: 1 path: application/forms/SetAsBackendForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\SetAsBackendForm\:\:assemble\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/forms/SetAsBackendForm.php - message: '#^Method Icinga\\Module\\Icingadb\\Forms\\SetAsBackendForm\:\:onSuccess\(\) has no return type specified\.$#' identifier: missingType.return count: 1 path: application/forms/SetAsBackendForm.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: '#^Method Icinga\\Module\\Icingadb\\Command\\Object\\ObjectsCommand\:\:getObjects\(\) return type has no value type specified in iterable type Traversable\.$#' identifier: missingType.iterableValue count: 1 path: library/Icingadb/Command/Object/ObjectsCommand.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: '#^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: '#^Cannot access offset ''in_downtime'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: '#^Cannot access offset ''is_acknowledged'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 1 path: library/Icingadb/Common/IcingaRedis.php - message: '#^Cannot access offset ''state_type'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible count: 2 path: library/Icingadb/Common/IcingaRedis.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: '#^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 \$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: '#^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 columns\(\) on mixed\.$#' identifier: method.nonObject count: 1 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: '#^Parameter \#2 \$value of static method ipl\\Stdlib\\Filter\:\:equal\(\) expects array\|bool\|float\|int\|string, mixed given\.$#' identifier: argument.type count: 3 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: '#^Property Icinga\\Module\\Monitoring\\Object\\MonitoredObject\:\:\$eventhistory \(Icinga\\Module\\Monitoring\\DataView\\EventHistory\) does not accept array\.$#' identifier: assign.propertyType count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Property Icinga\\Module\\Monitoring\\Object\\MonitoredObject\:\:\$hostVariables \(array\) in isset\(\) is not nullable\.$#' identifier: isset.property count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Property Icinga\\Module\\Monitoring\\Object\\MonitoredObject\:\:\$serviceVariables \(array\) in isset\(\) is not nullable\.$#' identifier: isset.property count: 1 path: library/Icingadb/Compat/CompatHost.php - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable count: 2 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 columns\(\) on mixed\.$#' identifier: method.nonObject count: 1 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\:\:getHost\(\) should return Icinga\\Module\\Icingadb\\Compat\\CompatHost but returns Icinga\\Module\\Monitoring\\Object\\Host\.$#' identifier: return.type 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: '#^Parameter \#2 \$value of static method ipl\\Stdlib\\Filter\:\:equal\(\) expects array\|bool\|float\|int\|string, mixed given\.$#' identifier: argument.type count: 3 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: '#^Property Icinga\\Module\\Monitoring\\Object\\MonitoredObject\:\:\$eventhistory \(Icinga\\Module\\Monitoring\\DataView\\EventHistory\) does not accept array\.$#' identifier: assign.propertyType count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Property Icinga\\Module\\Monitoring\\Object\\MonitoredObject\:\:\$hostVariables \(array\) in isset\(\) is not nullable\.$#' identifier: isset.property count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Property Icinga\\Module\\Monitoring\\Object\\MonitoredObject\:\:\$serviceVariables \(array\) in isset\(\) is not nullable\.$#' identifier: isset.property count: 1 path: library/Icingadb/Compat/CompatService.php - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable count: 2 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 \$column of method ipl\\Stdlib\\Filter\\Condition\:\:setColumn\(\) expects string, int\|string\|null given\.$#' identifier: argument.type count: 1 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 \$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: '#^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 \$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: '#^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: '#^Cannot cast mixed to string\.$#' identifier: cast.string count: 1 path: library/Icingadb/Model/Behavior/BoolCast.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: '#^Parameter \#2 \$value of static method ipl\\Stdlib\\Filter\:\:equal\(\) expects array\|bool\|float\|int\|string, mixed 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 \#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 \$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: '#^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 access property \$flatname on array\\.$#' identifier: property.nonObject 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: '#^Method Icinga\\Module\\Icingadb\\Web\\Controller\:\:sendAsPdf\(\) has no return type specified\.$#' identifier: missingType.return 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 \#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: '#^Parameter \#2 \$value of method ipl\\Html\\Attributes\:\:add\(\) expects array\|bool\|string\|null, int 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 \#1 \$view of method Icinga\\Module\\Monitoring\\Hook\\DetailviewExtensionHook\:\:setView\(\) expects Icinga\\Web\\View, null given\.$#' identifier: argument.type count: 1 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: '#^Possibly invalid array key type array\|bool\|string\|null\.$#' identifier: offsetAccess.invalidOffset count: 1 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.2.1/phpstan.neon000066400000000000000000000013761502521401400163650ustar00rootroot00000000000000includes: - phpstan-baseline-standard.neon - phpstan-baseline-by-php-version.php 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.2.1/phpunit.xml000066400000000000000000000010061502521401400162260ustar00rootroot00000000000000 test/php icingadb-web-1.2.1/public/000077500000000000000000000000001502521401400152765ustar00rootroot00000000000000icingadb-web-1.2.1/public/css/000077500000000000000000000000001502521401400160665ustar00rootroot00000000000000icingadb-web-1.2.1/public/css/common.less000066400000000000000000000114671502521401400202570ustar00rootroot00000000000000/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ @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); } .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.2.1/public/css/form/000077500000000000000000000000001502521401400170315ustar00rootroot00000000000000icingadb-web-1.2.1/public/css/form/schedule-service-downtime-form.less000066400000000000000000000004551502521401400257440ustar00rootroot00000000000000.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.2.1/public/css/item/000077500000000000000000000000001502521401400170245ustar00rootroot00000000000000icingadb-web-1.2.1/public/css/item/downtime.less000066400000000000000000000025301502521401400215420ustar00rootroot00000000000000// 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.2.1/public/css/item/hostgroup.less000066400000000000000000000006461502521401400217540ustar00rootroot00000000000000.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.2.1/public/css/item/icon-image.less000066400000000000000000000010371502521401400217250ustar00rootroot00000000000000.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.2.1/public/css/item/item-layout.less000066400000000000000000000025171502521401400221720ustar00rootroot00000000000000// 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.2.1/public/css/item/redundancy-group.less000066400000000000000000000004021502521401400231760ustar00rootroot00000000000000.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.2.1/public/css/item/servicegroup.less000066400000000000000000000000401502521401400224230ustar00rootroot00000000000000.servicegroup { .hostgroup; } icingadb-web-1.2.1/public/css/list/000077500000000000000000000000001502521401400170415ustar00rootroot00000000000000icingadb-web-1.2.1/public/css/list/action-list.less000066400000000000000000000003721502521401400221610ustar00rootroot00000000000000.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; } } icingadb-web-1.2.1/public/css/list/item-list.less000066400000000000000000000022561502521401400216450ustar00rootroot00000000000000// 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.2.1/public/css/list/state-item-table.less000066400000000000000000000063301502521401400230740ustar00rootroot00000000000000// 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.2.1/public/css/list/state-row-item.less000066400000000000000000000010661502521401400226150ustar00rootroot00000000000000// 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.2.1/public/css/markdown.less000066400000000000000000000017771502521401400206140ustar00rootroot00000000000000.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.2.1/public/css/mixin/000077500000000000000000000000001502521401400172125ustar00rootroot00000000000000icingadb-web-1.2.1/public/css/mixin/progress-bar.less000066400000000000000000000113501502521401400225100ustar00rootroot00000000000000.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.2.1/public/css/mixin/state-badges.less000066400000000000000000000010231502521401400224410ustar00rootroot00000000000000.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.2.1/public/css/mixins.less000066400000000000000000000002231502521401400202620ustar00rootroot00000000000000/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ .monospace() { font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; } icingadb-web-1.2.1/public/css/view/000077500000000000000000000000001502521401400170405ustar00rootroot00000000000000icingadb-web-1.2.1/public/css/view/service-grid.less000066400000000000000000000014731502521401400223200ustar00rootroot00000000000000.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.2.1/public/css/widget/000077500000000000000000000000001502521401400173515ustar00rootroot00000000000000icingadb-web-1.2.1/public/css/widget/actions.less000066400000000000000000000005411502521401400217010ustar00rootroot00000000000000.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.2.1/public/css/widget/check-attempt.less000066400000000000000000000004041502521401400227700ustar00rootroot00000000000000.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.2.1/public/css/widget/check-statistics.less000066400000000000000000000065271502521401400235200ustar00rootroot00000000000000.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.2.1/public/css/widget/comment-popup.less000066400000000000000000000025441502521401400230510ustar00rootroot00000000000000.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.2.1/public/css/widget/custom-var-table.less000066400000000000000000000015211502521401400234250ustar00rootroot00000000000000.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; } } } &.can-collapse thead th > span, // Icinga Web 2 < 2.12 &[data-can-collapse] thead th > span { // >= 2.12 :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.2.1/public/css/widget/dependency-node-state-badges.less000066400000000000000000000002211502521401400256360ustar00rootroot00000000000000.dependency-node-state-badges { .state-badges(); .state-badge { &.state-problem { background-color: @color-critical; } } } icingadb-web-1.2.1/public/css/widget/donut-container.less000066400000000000000000000004271502521401400233550ustar00rootroot00000000000000.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.2.1/public/css/widget/downtime-card.less000066400000000000000000000002431502521401400227750ustar00rootroot00000000000000.downtime-progress { .progress-bar(); &.progress-bar .timeline .downtime-elapsed { &::before, > .bar { background-color: @state-ok; } } } icingadb-web-1.2.1/public/css/widget/host-state-badges.less000066400000000000000000000000521502521401400235540ustar00rootroot00000000000000.host-state-badges { .state-badges(); } icingadb-web-1.2.1/public/css/widget/key-value-list.less000066400000000000000000000003431502521401400231140ustar00rootroot00000000000000.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.2.1/public/css/widget/migrate-popup.less000066400000000000000000000064111502521401400230340ustar00rootroot00000000000000#migrate-popup { @transitionLength: 350ms; display: flex; min-width: 16em; z-index: 1000; position: fixed; top: 0; right: 4em; pointer-events: none; line-height: 1.5em; .transform(translateY(-100%)); .transition(transform @transitionLength ease-in); &.active { .transform(translateY(0%)); .transition(transform @transitionLength ease-out); } .suggestion-area { .transform(translateY(0%)); .transition(transform 0s linear @transitionLength); } &.active .suggestion-area { .transition(transform @transitionLength ease-out); } &.minimized .suggestion-area { .transform(translateY(-100%)); .transition(transform @transitionLength ease-in); } &.hidden .suggestion-area { .transition(none); } .minimizer { width: 1.25em; height: 1.5em; margin-left: -1px; z-index: 1; pointer-events: auto; border-bottom-right-radius: 4px; background-color: @body-bg-color; .transition(none); i:before { width: 1em; margin: .1em 0 0 0; content: '\f102'; font-size: 1.25em; cursor: pointer; color: @gray-light; } i:hover:before { color: @menu-highlight-color; } } &.minimized .minimizer { border-bottom-left-radius: 4px; .transition(border-bottom-left-radius 0s linear @transitionLength); } &.hidden .minimizer i:before { content: '\f103'; } &:not(.active) .suggestion-area, &.hidden .suggestion-area { box-shadow: none; } .suggestion-area { display: flex; flex-direction: column-reverse; padding: .75em; flex-grow: 1; pointer-events: auto; font-size: .75em; background: @body-bg-color; border-radius: 0 0 4px 4px; box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.3); button { .link-button(); } p { display: none; margin-bottom: .5em; color: @text-color-light; } form ~ .monitoring-migration-hint, .search-migration-suggestions:not(:empty) + .search-migration-hint, .monitoring-migration-suggestions:not(:empty) + .monitoring-migration-hint { display: block; } & > button.close { margin-left: auto; margin-top: 1em; &:hover { text-decoration: underline; } } ul { padding: 0; margin: 0; list-style-type: none; } li { margin: .5em 0; display: flex; &:last-of-type { margin-bottom: 0; } &:first-of-type { margin-top: 0; } } li { :not(:last-child) { margin-right: .5em; } button:hover{ opacity: 0.8; } button[value="1"] { flex-grow: 1; color: @text-color; text-decoration: underline; } button[value="0"] { i:before { margin: 0; content: '\e804'; } } } form { width: 100%; .control-group { display: flex; align-items: center; .control-label-group { margin-right: .5em; } label { margin-left: auto; } } } .search-migration-suggestions:not(:empty) ~ form, .search-migration-suggestions:not(:empty) ~ .monitoring-migration-suggestions:not(:empty) { margin-bottom: .5em; } } } icingadb-web-1.2.1/public/css/widget/monitoring-health.less000066400000000000000000000037661502521401400237050ustar00rootroot00000000000000.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.2.1/public/css/widget/notice.less000066400000000000000000000006121502521401400215210ustar00rootroot00000000000000// 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.2.1/public/css/widget/object-features.less000066400000000000000000000014721502521401400233270ustar00rootroot00000000000000form.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.2.1/public/css/widget/object-grid.less000066400000000000000000000011221502521401400224260ustar00rootroot00000000000000// 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.2.1/public/css/widget/object-inspection.less000066400000000000000000000003311502521401400236550ustar00rootroot00000000000000// 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.2.1/public/css/widget/object-meta-info.less000066400000000000000000000026441502521401400233720ustar00rootroot00000000000000// 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.2.1/public/css/widget/object-statistics.less000066400000000000000000000012621502521401400237000ustar00rootroot00000000000000ul.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.2.1/public/css/widget/performance-data-table.less000066400000000000000000000016671502521401400245500ustar00rootroot00000000000000/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */ .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.2.1/public/css/widget/quick-actions.less000066400000000000000000000013411502521401400230120ustar00rootroot00000000000000.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.2.1/public/css/widget/service-state-badges.less000066400000000000000000000000551502521401400242420ustar00rootroot00000000000000.service-state-badges { .state-badges(); } icingadb-web-1.2.1/public/css/widget/state-change.less000066400000000000000000000044171502521401400226120ustar00rootroot00000000000000.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.2.1/public/css/widget/tag-list.less000066400000000000000000000011671502521401400217720ustar00rootroot00000000000000.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.2.1/public/css/widget/view-mode-switcher.less000066400000000000000000000014461502521401400237700ustar00rootroot00000000000000.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.2.1/public/js/000077500000000000000000000000001502521401400157125ustar00rootroot00000000000000icingadb-web-1.2.1/public/js/action-list.js000066400000000000000000000744001502521401400205030ustar00rootroot00000000000000/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ ;(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.2.1/public/js/migrate.js000066400000000000000000000602671502521401400177130ustar00rootroot00000000000000/* Icinga DB Web | (c) 2020 Icinga GmbH | GPLv2 */ ;(function(Icinga, $) { 'use strict'; const ANIMATION_LENGTH = 350; const POPUP_HTML = '
\n' + '
\n' + '
\n' + ' \n' + '
    \n' + '

    Miss some results? Try the link(s) below

    \n' + '
      \n' + '

      Preview this in Icinga DB

      \n' + '
      \n' + '
      \n' + '
      \n' + '
      '; const SUGGESTION_HTML = '
    • \n' + ' \n' + ' \n' + '
    • '; Icinga.Behaviors = Icinga.Behaviors || {}; /** * Icinga DB Migration behavior. * * @param icinga {Icinga} The current Icinga Object */ class Migrate extends Icinga.EventListener { constructor(icinga) { super(icinga); this.knownMigrations = {}; this.knownBackendSupport = {}; this.urlMigrationReadyState = null; this.backendSupportReadyState = null; this.searchMigrationReadyState = null; this.backendSupportRelated = {}; this.$popup = null; // Some persistence, we don't want to annoy our users too much this.storage = Icinga.Storage.BehaviorStorage('icingadb.migrate'); this.tempStorage = Icinga.Storage.BehaviorStorage('icingadb.migrate'); this.tempStorage.setBackend(window.sessionStorage); this.previousMigrations = {}; // We don't want to ask the server to migrate non-monitoring urls this.isMonitoringUrl = new RegExp('^' + icinga.config.baseUrl + '/monitoring/'); this.on('rendered', this.onRendered, this); this.on('close-column', this.onColumnClose, this); this.on('click', '#migrate-popup button.close', this.onClose, this); this.on('click', '#migrate-popup li button', this.onDecision, this); this.on('click', '#migrate-popup .minimizer', this.onHandleClicked, this); this.storage.onChange('minimized', this.onMinimized, this); } update(data) { if (data !== 'bogus') { return; } $.each(this.backendSupportRelated, (id, _) => { let $container = $('#' + id); let req = this.icinga.loader.loadUrl($container.data('icingaUrl'), $container); req.addToHistory = false; req.scripted = true; }); } onRendered(event) { var _this = event.data.self; var $target = $(event.target); if (_this.tempStorage.get('closed') || $('#layout.fullscreen-layout').length) { // Don't bother in case the user closed the popup or we're in fullscreen return; } if (!$target.is('#main > .container')) { if ($target.is('#main .container')) { var attrUrl = $target.attr('data-icinga-url'); var dataUrl = $target.data('icingaUrl'); if (!! attrUrl && attrUrl !== dataUrl) { // Search urls are redirected, update any migration suggestions _this.prepareMigration($target); return; } } // We are else really only interested in top-level containers return; } var $dashboard = $target.children('.dashboard'); if ($dashboard.length) { // After a page load dashlets have no id as `renderContentToContainer()` didn't ran yet _this.icinga.ui.assignUniqueContainerIds(); $target = $dashboard.children('.container'); } _this.prepareMigration($target); } prepareMigration($target) { let monitoringUrls = {}; let searchUrls = {}; let modules = {} $target.each((_, container) => { let $container = $(container); let href = decodeURIComponent($container.data('icingaUrl')); let containerId = $container.attr('id'); if (!!href) { if ( typeof this.previousMigrations[containerId] !== 'undefined' && this.previousMigrations[containerId] === href ) { delete this.previousMigrations[containerId]; } else { if (href.match(this.isMonitoringUrl)) { monitoringUrls[containerId] = href; } else if ($container.find('[data-enrichment-type="search-bar"]').length) { searchUrls[containerId] = href; } } } let moduleName = $container.data('icingaModule'); if (!! moduleName && moduleName !== 'default' && moduleName !== 'monitoring' && moduleName !== 'icingadb') { modules[containerId] = moduleName; } }); if (Object.keys(monitoringUrls).length) { this.setUrlMigrationReadyState(false); this.migrateUrls(monitoringUrls, 'monitoring'); } else { this.setUrlMigrationReadyState(null); } if (Object.keys(searchUrls).length) { this.setSearchMigrationReadyState(false); this.migrateUrls(searchUrls, 'search'); } else { this.setSearchMigrationReadyState(null); } if (Object.keys(modules).length) { this.setBackendSupportReadyState(false); this.prepareBackendCheckboxForm(modules); } else { this.setBackendSupportReadyState(null); } if ( this.urlMigrationReadyState === null && this.backendSupportReadyState === null && this.searchMigrationReadyState === null ) { this.cleanupPopup(); } } onColumnClose(event) { var _this = event.data.self; _this.Popup().find('.suggestion-area > ul li').each(function () { var $suggestion = $(this); var suggestionUrl = $suggestion.data('containerUrl'); var $container = $('#' + $suggestion.data('containerId')); var containerUrl = ''; if ($container.length) { containerUrl = decodeURIComponent($container.data('icingaUrl')); } if (suggestionUrl !== containerUrl) { var $newContainer = $('#main > .container').filter(function () { return decodeURIComponent($(this).data('icingaUrl')) === suggestionUrl; }); if ($newContainer.length) { // Container moved $suggestion.attr('id', 'suggest-' + $newContainer.attr('id')); $suggestion.data('containerId', $newContainer.attr('id')); } } }); let backendSupportRelated = { ..._this.backendSupportRelated }; $.each(backendSupportRelated, (id, module) => { let $container = $('#' + id); if (! $container.length || $container.data('icingaModule') !== module) { let $newContainer = $('#main > .container').filter(function () { return $(this).data('icingaModule') === module; }); if ($newContainer.length) { _this.backendSupportRelated[$newContainer.attr('id')] = module; } delete _this.backendSupportRelated[id]; } }); _this.cleanupPopup(); } onClose(event) { var _this = event.data.self; _this.tempStorage.set('closed', true); _this.hidePopup(); } onDecision(event) { var _this = event.data.self; var $button = $(event.target).closest('button'); var $suggestion = $button.parent(); var $container = $('#' + $suggestion.data('containerId')); var containerUrl = decodeURIComponent($container.data('icingaUrl')); if ($button.attr('value') === '1') { // Yes var newHref = _this.knownMigrations[containerUrl]; _this.icinga.loader.loadUrl(newHref, $container); _this.previousMigrations[$suggestion.data('containerId')] = containerUrl; if ($container.parent().is('.dashboard')) { $container.find('h1 a').attr('href', _this.icinga.utils.removeUrlParams(newHref, ['showCompact'])); } } else { // No _this.knownMigrations[containerUrl] = false; } if (_this.Popup().find('li').length === 1 && ! _this.Popup().find('#setAsBackendForm').length) { _this.hidePopup(function () { // Let the transition finish first, looks cleaner $suggestion.remove(); }); } else { $suggestion.remove(); } } onHandleClicked(event) { var _this = event.data.self; if (_this.togglePopup()) { _this.storage.set('minimized', true); } else { _this.storage.remove('minimized'); } } onMinimized(isMinimized, oldValue) { if (isMinimized && isMinimized !== oldValue && this.isShown()) { this.minimizePopup(); } } migrateUrls(urls, type) { var _this = this, containerIds = [], containerUrls = []; $.each(urls, function (containerId, containerUrl) { if (typeof _this.knownMigrations[containerUrl] === 'undefined') { containerUrls.push(containerUrl); containerIds.push(containerId); } }); let endpoint, changeCallback; if (type === 'monitoring') { endpoint = 'monitoring-url'; changeCallback = this.changeUrlMigrationReadyState.bind(this); } else { endpoint = 'search-url'; changeCallback = this.changeSearchMigrationReadyState.bind(this); } if (containerUrls.length) { var req = $.ajax({ context: this, type: 'post', url: this.icinga.config.baseUrl + '/icingadb/migrate/' + endpoint, headers: {'Accept': 'application/json'}, contentType: 'application/json', data: JSON.stringify(containerUrls) }); req.urls = urls; req.suggestionType = type; req.urlIndexToContainerId = containerIds; req.done(this.processUrlMigrationResults); req.always(() => changeCallback(true)); } else { // All urls have already been migrated once, show popup immediately this.addSuggestions(urls, type); changeCallback(true); } } processUrlMigrationResults(data, textStatus, req) { var _this = this; var result, containerId; if (data.status === 'success') { result = data.data; } else { // if (data.status === 'fail') result = data.data.result; $.each(data.data.errors, function (k, error) { _this.icinga.logger.error('[Migrate] Erroneous url "' + k + '": ' + error[0] + '\n' + error[1]); }); } $.each(result, function (i, migratedUrl) { containerId = req.urlIndexToContainerId[i]; _this.knownMigrations[req.urls[containerId]] = migratedUrl; }); this.addSuggestions(req.urls, req.suggestionType); } prepareBackendCheckboxForm(modules) { let containerIds = []; let moduleNames = []; $.each(modules, (id, module) => { if (typeof this.knownBackendSupport[module] === 'undefined') { containerIds.push(id); moduleNames.push(module); } }); if (moduleNames.length) { let req = $.ajax({ context : this, type : 'post', url : this.icinga.config.baseUrl + '/icingadb/migrate/backend-support', headers : { 'Accept': 'application/json' }, contentType : 'application/json', data : JSON.stringify(moduleNames) }); req.modules = modules; req.moduleIndexToContainerId = containerIds; req.done(this.processBackendSupportResults); req.always(() => this.changeBackendSupportReadyState(true)); } else { // All modules have already been checked once, show popup immediately this.setupBackendCheckboxForm(modules); this.changeBackendSupportReadyState(true); } } processBackendSupportResults(data, textStatus, req) { let result = data.data; $.each(result, (i, state) => { let containerId = req.moduleIndexToContainerId[i]; this.knownBackendSupport[req.modules[containerId]] = state; }); this.setupBackendCheckboxForm(req.modules); } setupBackendCheckboxForm(modules) { let supportedModules = {}; $.each(modules, (id, module) => { if (this.knownBackendSupport[module]) { supportedModules[id] = module; } }); if (Object.keys(supportedModules).length) { this.backendSupportRelated = { ...this.backendSupportRelated, ...supportedModules }; let req = $.ajax({ context : this, type : 'get', url : this.icinga.config.baseUrl + '/icingadb/migrate/checkbox-state?showCompact' }); req.done(this.setCheckboxState); } } setCheckboxState(html, textStatus, req) { let $form = this.Popup().find('.suggestion-area > #setAsBackendForm'); if (! $form.length) { $form = $(html); $form.attr('data-base-target', 'migrate-popup-backend-submit-blackhole'); $form.append('
      '); this.Popup().find('.monitoring-migration-suggestions').before($form); } else { let $newForm = $(html); $form.find('[name=backend]').prop('checked', $newForm.find('[name=backend]').is(':checked')); } this.showPopup(); } addSuggestions(urls, type) { var where; if (type === 'monitoring') { where = '.monitoring-migration-suggestions'; } else { where = '.search-migration-suggestions'; } var _this = this, hasSuggestions = false, $ul = this.Popup().find('.suggestion-area > ul' + where); $.each(urls, function (containerId, containerUrl) { // No urls for which the user clicked "No" or an error occurred and only migrated urls please if (_this.knownMigrations[containerUrl] !== false && _this.knownMigrations[containerUrl] !== containerUrl) { var $container = $('#' + containerId); var $suggestion = $ul.find('li#suggest-' + containerId); if ($suggestion.length) { if ($suggestion.data('containerUrl') === containerUrl) { // There's already a suggestion for this exact container and url hasSuggestions = true; return; } $suggestion.data('containerUrl', containerUrl); } else { $suggestion = $(SUGGESTION_HTML); $suggestion.attr('id', 'suggest-' + containerId); $suggestion.data('containerId', containerId); $suggestion.data('containerUrl', containerUrl); $ul.append($suggestion); } hasSuggestions = true; var title; if ($container.data('icingaTitle')) { title = $container.data('icingaTitle').split(' :: ').slice(0, -1).join(' :: '); } else if ($container.parent().is('.dashboard')) { title = $container.find('h1 a').text(); } else { title = $container.find('.tabs li.active a').text(); } $suggestion.find('button:first-of-type').text(title); } }); if (hasSuggestions) { this.showPopup(); if (type === 'search') { this.maximizePopup(); } } } cleanupSuggestions() { var _this = this, toBeRemoved = []; this.Popup().find('li').each(function () { var $suggestion = $(this); var $container = $('#' + $suggestion.data('containerId')); var containerUrl = decodeURIComponent($container.data('icingaUrl')); if ( // Unknown url, yet typeof _this.knownMigrations[containerUrl] === 'undefined' // User doesn't want to migrate || _this.knownMigrations[containerUrl] === false // Already migrated or no migration necessary || containerUrl === _this.knownMigrations[containerUrl] // The container URL changed || containerUrl !== $suggestion.data('containerUrl') ) { toBeRemoved.push($suggestion); } }); return toBeRemoved; } cleanupBackendForm() { let $form = this.Popup().find('#setAsBackendForm'); if (! $form.length) { return false; } let stillRelated = {}; $.each(this.backendSupportRelated, (id, module) => { let $container = $('#' + id); if ($container.length && $container.data('icingaModule') === module) { stillRelated[id] = module; } }); this.backendSupportRelated = stillRelated; if (Object.keys(stillRelated).length) { return true; } return $form; } cleanupPopup() { let toBeRemoved = this.cleanupSuggestions(); let hasBackendForm = this.cleanupBackendForm(); if (hasBackendForm !== true && this.Popup().find('li').length === toBeRemoved.length) { this.hidePopup(() => { // Let the transition finish first, looks cleaner $.each(toBeRemoved, function (_, $suggestion) { $suggestion.remove(); }); if (typeof hasBackendForm === 'object') { hasBackendForm.remove(); } }); } else { $.each(toBeRemoved, function (_, $suggestion) { $suggestion.remove(); }); if (typeof hasBackendForm === 'object') { hasBackendForm.remove(); } // Let showPopup() handle the automatic minimization in case all search suggestions have been removed this.showPopup(); } } showPopup() { var $popup = this.Popup(); if (this.storage.get('minimized') && ! this.forceFullyMaximized()) { if (this.isShown()) { this.minimizePopup(); } else { $popup.addClass('active minimized hidden'); } } else { $popup.addClass('active'); } } hidePopup(after) { this.Popup().removeClass('active minimized hidden'); if (typeof after === 'function') { setTimeout(after, ANIMATION_LENGTH); } } isShown() { return this.Popup().is('.active'); } minimizePopup() { var $popup = this.Popup(); $popup.addClass('minimized'); setTimeout(function () { $popup.addClass('hidden'); }, ANIMATION_LENGTH); } maximizePopup() { this.Popup().removeClass('minimized hidden'); } forceFullyMaximized() { return this.Popup().find('.search-migration-suggestions:not(:empty)').length > 0; } togglePopup() { if (this.Popup().is('.minimized')) { this.maximizePopup(); return false; } else { this.minimizePopup(); return true; } } setUrlMigrationReadyState(state) { this.urlMigrationReadyState = state; } changeUrlMigrationReadyState(state) { this.setUrlMigrationReadyState(state); if (this.backendSupportReadyState !== false && this.searchMigrationReadyState !== false) { this.searchMigrationReadyState = null; this.backendSupportReadyState = null; this.urlMigrationReadyState = null; this.cleanupPopup(); } } setSearchMigrationReadyState(state) { this.searchMigrationReadyState = state; } changeSearchMigrationReadyState(state) { this.setSearchMigrationReadyState(state); if (this.backendSupportReadyState !== false && this.urlMigrationReadyState !== false) { this.searchMigrationReadyState = null; this.backendSupportReadyState = null; this.urlMigrationReadyState = null; this.cleanupPopup(); } } setBackendSupportReadyState(state) { this.backendSupportReadyState = state; } changeBackendSupportReadyState(state) { this.setBackendSupportReadyState(state); if (this.urlMigrationReadyState !== false && this.searchMigrationReadyState !== false) { this.searchMigrationReadyState = null; this.backendSupportReadyState = null; this.urlMigrationReadyState = null; this.cleanupPopup(); } } Popup() { // Node.contains() is used due to `?renderLayout` if (this.$popup === null || ! document.body.contains(this.$popup[0])) { $('#layout').append($(POPUP_HTML)); this.$popup = $('#migrate-popup'); } return this.$popup; } } Icinga.Behaviors.Migrate = Migrate; })(Icinga, jQuery); icingadb-web-1.2.1/public/js/progress-bar.js000066400000000000000000000071111502521401400206560ustar00rootroot00000000000000(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.2.1/run.php000066400000000000000000000032561502521401400153430ustar00rootroot00000000000000provideHook('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('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')) { $modulePath = null; foreach ($this->app->getModuleManager()->getModuleDirs() as $path) { $pathToTest = join(DIRECTORY_SEPARATOR, [$path, 'monitoring']); if (file_exists($pathToTest)) { $modulePath = $pathToTest; break; } } if ($modulePath === null) { Icinga\Application\Logger::error('Unable to locate monitoring module'); } else { // Ensure we can load some classes/interfaces for compatibility with legacy hooks $this->app->getLoader()->registerNamespace( 'Icinga\\Module\\Monitoring', join(DIRECTORY_SEPARATOR, [$modulePath, 'library', 'Monitoring']), join(DIRECTORY_SEPARATOR, [$modulePath, 'application']) ); } } icingadb-web-1.2.1/test/000077500000000000000000000000001502521401400147775ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/000077500000000000000000000000001502521401400155665ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/Lib/000077500000000000000000000000001502521401400162745ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/Lib/PerfdataSetWithPublicData.php000066400000000000000000000003511502521401400237730ustar00rootroot00000000000000 ['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.2.1/test/php/application/000077500000000000000000000000001502521401400200715ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/application/clicommands/000077500000000000000000000000001502521401400223625ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/application/clicommands/MigrateCommandTest.php000066400000000000000000002147421502521401400266340ustar00rootroot00000000000000 [ '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.2.1/test/php/library/000077500000000000000000000000001502521401400172325ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/library/Icingadb/000077500000000000000000000000001502521401400207325ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/library/Icingadb/Command/000077500000000000000000000000001502521401400223105ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/library/Icingadb/Command/Transport/000077500000000000000000000000001502521401400243045ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/library/Icingadb/Command/Transport/CommandTransportTest.php000066400000000000000000000033351502521401400311540ustar00rootroot00000000000000expectException(CommandTransportException::class); $this->expectExceptionMessage('endpointB 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 testGeneratorsAreNotSupported() { $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']); })()) ); } } icingadb-web-1.2.1/test/php/library/Icingadb/Common/000077500000000000000000000000001502521401400221625ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/library/Icingadb/Common/MacrosTest.php000066400000000000000000000146601502521401400247660ustar00rootroot00000000000000 "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.2.1/test/php/library/Icingadb/Common/StateBadgesTest.php000066400000000000000000000046331502521401400257270ustar00rootroot00000000000000createStateBadges() ->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.2.1/test/php/library/Icingadb/Model/000077500000000000000000000000001502521401400217725ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/library/Icingadb/Model/Behavior/000077500000000000000000000000001502521401400235315ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/library/Icingadb/Model/Behavior/FlattenedObjectVarsTest.php000066400000000000000000000164521502521401400310030ustar00rootroot00000000000000= ?)) 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_host_customvar ON sub_customvar_flat_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_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_host_customvar ON sub_customvar_flat_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_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_host_customvar ON sub_customvar_flat_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_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_host_customvar ON sub_customvar_flat_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_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.2.1/test/php/library/Icingadb/Model/CustomvarFlatTest.php000066400000000000000000000072671502521401400261510ustar00rootroot00000000000000 [ "not_empty" => [ "foo" => "bar" ], "empty" => [] ], "list" => [ ["foo", "bar"], [] ], "empty_list" => [], "empty_dict" => [], "null" => "null" ]; 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\""] ]; 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.2.1/test/php/library/Icingadb/ProvidedHook/000077500000000000000000000000001502521401400233275ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/library/Icingadb/ProvidedHook/IcingaHealthTest.php000066400000000000000000000011761502521401400272250ustar00rootroot00000000000000assertEquals('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.2.1/test/php/library/Icingadb/Util/000077500000000000000000000000001502521401400216475ustar00rootroot00000000000000icingadb-web-1.2.1/test/php/library/Icingadb/Util/PerfdataSetTest.php000066400000000000000000000076701502521401400254340ustar00rootroot00000000000000assertSame( '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.2.1/test/php/library/Icingadb/Util/PerfdataTest.php000066400000000000000000000465611502521401400247620ustar00rootroot00000000000000expectException(\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.2.1/test/php/library/Icingadb/Util/PluginOutputTest.php000066400000000000000000000130201502521401400256730ustar00rootroot00000000000000setCharacterLimit($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.2.1/test/php/library/Icingadb/Util/ThresholdRangeTest.php000066400000000000000000000312701502521401400261340ustar00rootroot00000000000000assertSame( 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.2.1/doc/res/ListItemAnatomyDetailed.jpg000066400000000000000000002002441502521401400226010ustar00rootroot00000000000000JFIFHH@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.2.1/doc/res/ListItemDowntimeAnatomy.jpg000066400000000000000000001747471502521401400226760ustar00rootroot00000000000000JFIFHH@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.2.1/doc/res/continue-with-preview.png000066400000000000000000002245031502521401400223460ustar00rootroot00000000000000PNG  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.2.1/doc/res/icingadb-architecture.png000066400000000000000000015672211502521401400223220ustar00rootroot00000000000000PNG  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.2.1/doc/res/icingadb-dashboard.png000066400000000000000000007404141502521401400215630ustar00rootroot00000000000000PNG  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.2.1/doc/res/icingadb-web.png000066400000000000000000014755101502521401400204140ustar00rootroot00000000000000PNG  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.2.1/doc/res/service-detail-preview.png000066400000000000000000004561741502521401400224640ustar00rootroot00000000000000PNG  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.2.1/doc/res/view-switcher-preview.png000066400000000000000000003115241502521401400223510ustar00rootroot00000000000000PNG  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&