From 961a6492f9e5190f00bf9e73c8082e538aac2fc5 Mon Sep 17 00:00:00 2001 From: Hex Date: Thu, 30 May 2024 14:21:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E5=88=B0=204.5.1=20(#164)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 升级到 4.5.1 --- source/changelogs/index.rst | 5 + source/changelogs/v4.4.6.rst | 34 + source/changelogs/v4.4.7.rst | 33 + source/changelogs/v4.4.8.rst | 26 + source/changelogs/v4.5.0.rst | 404 +++++++++++ source/changelogs/v4.5.1.rst | 19 + source/cli/cli_generators.rst | 26 +- source/concepts/autoloader.rst | 112 ++- source/concepts/autoloader/001.php | 3 +- source/concepts/factories.rst | 54 +- source/concepts/security.rst | 667 ++++++++++++++---- source/conf.py | 4 +- source/database/configuration.rst | 69 +- source/database/configuration/001.php | 4 +- source/database/configuration/004.php | 2 +- source/database/configuration/005.php | 8 +- source/database/configuration/006.php | 4 +- source/database/configuration/010.php | 2 +- source/database/configuration/011.php | 21 + source/database/connecting/006.php | 4 +- source/database/events.rst | 8 +- source/database/examples.rst | 2 +- source/database/metadata.rst | 31 +- source/database/queries.rst | 5 +- source/database/query_builder.rst | 2 +- source/database/results/016.php | 2 +- source/database/transactions.rst | 6 +- source/dbmgmt/db_commands.rst | 19 +- source/dbmgmt/forge.rst | 8 + source/dbmgmt/forge/016.php | 2 +- source/dbmgmt/migration.rst | 1 + source/dbmgmt/migration/004.php | 4 +- source/extending/events.rst | 31 +- source/general/caching.rst | 8 +- source/general/common_functions.rst | 10 +- source/general/configuration.rst | 70 +- source/general/errors.rst | 101 ++- source/general/errors/003.php | 4 +- source/general/errors/004.php | 4 +- source/general/errors/005.php | 4 +- source/general/errors/006.php | 4 +- source/general/errors/007.php | 8 +- source/general/managing_apps.rst | 2 + source/general/modules/001.php | 4 +- source/general/urls.rst | 39 + source/helpers/array_helper.rst | 28 +- source/helpers/cookie_helper.rst | 2 +- source/helpers/url_helper.rst | 16 +- source/helpers/url_helper/004.php | 1 + source/helpers/url_helper/026.php | 4 + source/images/error.png | Bin 87299 -> 123949 bytes source/incoming/controllers.rst | 36 +- source/incoming/controllers/015.php | 3 - source/incoming/filters.rst | 110 ++- source/incoming/filters/008.php | 4 +- source/incoming/filters/013.php | 23 + source/incoming/incomingrequest.rst | 14 +- source/incoming/incomingrequest/005.php | 2 +- source/incoming/incomingrequest/027.php | 3 - source/incoming/incomingrequest/028.php | 3 - source/incoming/incomingrequest/029.php | 4 - source/incoming/incomingrequest/030.php | 3 - source/incoming/incomingrequest/031.php | 3 - source/incoming/message.rst | 15 +- source/incoming/message/011.php | 4 + source/incoming/request.rst | 7 +- source/incoming/request/003.php | 4 +- source/incoming/routing.rst | 102 ++- source/incoming/routing/004.php | 2 +- source/incoming/routing/026.php | 6 +- source/incoming/routing/033.php | 2 +- source/incoming/routing/047.php | 4 - source/incoming/routing/048.php | 3 - source/incoming/routing/051.php | 9 - source/incoming/routing/069.php | 13 + source/incoming/routing/070.php | 11 + source/installation/deployment.rst | 183 +++++ source/installation/index.rst | 3 +- source/installation/installing_composer.rst | 81 ++- source/installation/installing_manual.rst | 2 +- source/installation/preloading/001.php | 20 + source/installation/running.rst | 69 +- source/installation/troubleshooting.rst | 7 +- source/installation/upgrade_415/001.php | 4 +- source/installation/upgrade_444.rst | 2 +- source/installation/upgrade_445.rst | 2 +- source/installation/upgrade_446.rst | 50 ++ source/installation/upgrade_447.rst | 126 ++++ source/installation/upgrade_448.rst | 31 + source/installation/upgrade_450.rst | 329 +++++++++ source/installation/upgrade_451.rst | 36 + source/installation/upgrade_4xx.rst | 27 +- source/installation/upgrade_configuration.rst | 4 +- source/installation/upgrade_controllers.rst | 4 +- source/installation/upgrade_database.rst | 5 +- source/installation/upgrade_emails.rst | 4 +- source/installation/upgrade_encryption.rst | 4 +- source/installation/upgrade_file_upload.rst | 4 +- .../installation/upgrade_file_upload/001.php | 2 +- source/installation/upgrade_html_tables.rst | 4 +- source/installation/upgrade_images.rst | 39 + source/installation/upgrade_images/001.php | 8 + .../upgrade_images/ci3sample/001.php | 12 + source/installation/upgrade_localization.rst | 4 +- source/installation/upgrade_migrations.rst | 4 +- source/installation/upgrade_models.rst | 4 +- source/installation/upgrade_pagination.rst | 4 +- source/installation/upgrade_responses.rst | 4 +- source/installation/upgrade_routing.rst | 4 +- source/installation/upgrade_security.rst | 4 +- source/installation/upgrade_sessions.rst | 4 +- source/installation/upgrade_validations.rst | 28 +- .../installation/upgrade_validations/001.php | 2 +- .../installation/upgrade_validations/002.php | 6 +- source/installation/upgrade_view_parser.rst | 4 +- source/installation/upgrade_views.rst | 4 +- source/installation/upgrading.rst | 5 + source/intro/index.rst | 15 +- source/intro/requirements.rst | 10 +- source/libraries/cookies/004.php | 2 +- source/libraries/cors.rst | 132 ++++ source/libraries/cors/001.php | 18 + source/libraries/cors/002.php | 19 + source/libraries/cors/003.php | 7 + source/libraries/cors/004.php | 25 + source/libraries/cors/005.php | 10 + source/libraries/curlrequest.rst | 12 +- source/libraries/curlrequest/014.php | 6 +- source/libraries/curlrequest/017.php | 2 +- source/libraries/curlrequest/018.php | 2 +- source/libraries/curlrequest/019.php | 2 +- source/libraries/curlrequest/025.php | 2 +- source/libraries/images.rst | 22 +- source/libraries/index.rst | 1 + source/libraries/official_packages.rst | 2 + source/libraries/security.rst | 6 +- source/libraries/security/009.php | 4 +- source/libraries/sessions.rst | 10 +- source/libraries/throttler/002.php | 8 +- source/libraries/throttler/004.php | 2 +- source/libraries/time.rst | 24 +- source/libraries/time/001.php | 4 +- source/libraries/time/042.php | 13 + source/libraries/uploaded_files.rst | 2 +- source/libraries/uploaded_files/002.php | 2 +- source/libraries/validation.rst | 67 +- source/libraries/validation/045.php | 2 +- source/libraries/validation/046.php | 43 ++ source/libraries/validation/047.php | 22 + source/license.rst | 2 +- source/models/entities.rst | 13 +- source/models/entities/018.php | 2 +- source/models/entities/020.php | 2 +- source/models/model.rst | 151 +++- source/models/model/003.php | 2 + source/models/model/004.php | 2 + source/models/model/005.php | 1 + source/models/model/027.php | 2 + source/models/model/034.php | 2 + source/models/model/038.php | 2 + source/models/model/040.php | 2 + source/models/model/041.php | 2 + source/models/model/050.php | 2 + source/models/model/051.php | 4 + source/models/model/052.php | 4 + source/models/model/054.php | 2 + source/models/model/057.php | 17 + source/models/model/058.php | 40 ++ source/models/model/059.php | 23 + source/models/model/060.php | 27 + source/models/model/061.php | 23 + source/models/model/062.php | 27 + source/outgoing/alternative_php.rst | 4 +- source/outgoing/alternative_php/001.php | 2 +- source/outgoing/api_responses.rst | 13 +- source/outgoing/csp.rst | 20 +- source/outgoing/csp/014.php | 6 + source/outgoing/localization.rst | 114 ++- source/outgoing/localization/019.php | 13 + source/outgoing/response.rst | 4 +- source/outgoing/response/011.php | 12 - source/outgoing/response/012.php | 33 - source/outgoing/response/013.php | 9 - source/outgoing/response/023.php | 2 +- source/outgoing/view_renderer.rst | 13 +- source/outgoing/views.rst | 14 +- source/testing/cli.rst | 144 ++++ source/testing/cli/001.php | 37 + source/testing/controllers/001.php | 4 +- source/testing/controllers/002.php | 6 +- source/testing/controllers/011.php | 4 +- source/testing/controllers/012.php | 4 +- source/testing/controllers/014.php | 4 +- source/testing/debugging.rst | 51 +- source/testing/fabricator.rst | 22 +- source/testing/fabricator/001.php | 10 + source/testing/fabricator/005.php | 18 +- source/testing/fabricator/006.php | 3 +- source/testing/fabricator/008.php | 3 +- source/testing/fabricator/021.php | 7 +- source/testing/fabricator/022.php | 11 + source/testing/fabricator/023.php | 20 + source/testing/feature.rst | 11 +- source/testing/feature/001.php | 5 +- source/testing/feature/002.php | 2 +- source/testing/feature/004.php | 2 +- source/testing/index.rst | 3 +- source/testing/overview.rst | 89 +-- source/testing/overview/002.php | 2 +- source/testing/overview/003.php | 2 +- source/testing/overview/016.php | 7 +- source/testing/overview/017.php | 3 +- source/testing/overview/018.php | 2 + source/testing/overview/019.php | 2 + source/testing/overview/020.php | 2 + source/testing/response.rst | 16 + source/testing/response/033.php | 11 + source/testing/response/034.php | 11 + source/tutorial/create_news_items/001.php | 2 +- source/tutorial/news_section.rst | 4 +- source/tutorial/news_section/002.php | 5 + source/tutorial/news_section/003.php | 4 +- source/tutorial/news_section/004.php | 4 +- source/tutorial/news_section/005.php | 4 +- source/tutorial/news_section/006.php | 4 +- source/tutorial/static_pages.rst | 6 +- source/tutorial/static_pages/001.php | 2 +- source/tutorial/static_pages/002.php | 5 +- 228 files changed, 4353 insertions(+), 780 deletions(-) create mode 100644 source/changelogs/v4.4.6.rst create mode 100644 source/changelogs/v4.4.7.rst create mode 100644 source/changelogs/v4.4.8.rst create mode 100644 source/changelogs/v4.5.0.rst create mode 100644 source/changelogs/v4.5.1.rst create mode 100644 source/database/configuration/011.php create mode 100644 source/helpers/url_helper/026.php delete mode 100644 source/incoming/controllers/015.php create mode 100644 source/incoming/filters/013.php delete mode 100644 source/incoming/incomingrequest/027.php delete mode 100644 source/incoming/incomingrequest/028.php delete mode 100644 source/incoming/incomingrequest/029.php delete mode 100644 source/incoming/incomingrequest/030.php delete mode 100644 source/incoming/incomingrequest/031.php create mode 100644 source/incoming/message/011.php delete mode 100644 source/incoming/routing/047.php delete mode 100644 source/incoming/routing/048.php create mode 100644 source/incoming/routing/069.php create mode 100644 source/incoming/routing/070.php create mode 100644 source/installation/deployment.rst create mode 100644 source/installation/preloading/001.php create mode 100644 source/installation/upgrade_446.rst create mode 100644 source/installation/upgrade_447.rst create mode 100644 source/installation/upgrade_448.rst create mode 100644 source/installation/upgrade_450.rst create mode 100644 source/installation/upgrade_451.rst create mode 100644 source/installation/upgrade_images.rst create mode 100644 source/installation/upgrade_images/001.php create mode 100644 source/installation/upgrade_images/ci3sample/001.php create mode 100644 source/libraries/cors.rst create mode 100644 source/libraries/cors/001.php create mode 100644 source/libraries/cors/002.php create mode 100644 source/libraries/cors/003.php create mode 100644 source/libraries/cors/004.php create mode 100644 source/libraries/cors/005.php create mode 100644 source/libraries/time/042.php create mode 100644 source/libraries/validation/046.php create mode 100644 source/libraries/validation/047.php create mode 100644 source/models/model/057.php create mode 100644 source/models/model/058.php create mode 100644 source/models/model/059.php create mode 100644 source/models/model/060.php create mode 100644 source/models/model/061.php create mode 100644 source/models/model/062.php create mode 100644 source/outgoing/csp/014.php create mode 100644 source/outgoing/localization/019.php delete mode 100644 source/outgoing/response/011.php delete mode 100644 source/outgoing/response/012.php delete mode 100644 source/outgoing/response/013.php create mode 100644 source/testing/cli.rst create mode 100644 source/testing/cli/001.php create mode 100644 source/testing/fabricator/022.php create mode 100644 source/testing/fabricator/023.php create mode 100644 source/testing/response/033.php create mode 100644 source/testing/response/034.php diff --git a/source/changelogs/index.rst b/source/changelogs/index.rst index 7f5e7d91..a8a000f9 100644 --- a/source/changelogs/index.rst +++ b/source/changelogs/index.rst @@ -12,6 +12,11 @@ .. toctree:: :titlesonly: + v4.5.1 + v4.5.0 + v4.4.8 + v4.4.7 + v4.4.6 v4.4.5 v4.4.4 v4.4.3 diff --git a/source/changelogs/v4.4.6.rst b/source/changelogs/v4.4.6.rst new file mode 100644 index 00000000..5f90656b --- /dev/null +++ b/source/changelogs/v4.4.6.rst @@ -0,0 +1,34 @@ +############# +版本 4.4.6 +############# + +发布日期:2024 年 2 月 24 日 + +**CodeIgniter4 的 4.4.6 版本发布** + +.. contents:: + :local: + :depth: 3 + +******** +重大变更 +******** + +Time::createFromTimestamp() +=========================== + +修复了一个导致 :ref:`Time::createFromTimestamp() ` 返回 UTC 时区的 Time 实例的错误。 + +从这个版本开始,当你不指定时区时,默认返回应用程序时区的 Time 实例。 + +********** +修复的错误 +********** + +- **Session:** 修复了 Redis session 处理程序中的一个错误,该错误导致锁定失败并清除了 session 数据。 +- **DB Forge:** 修复了 SQLite3 Forge 中的一个错误,该错误导致 ``Forge::modifyColumn()`` 错误地修改了表定义。 +- **CSP:** 修复了一个导致 CSP 阻止调试工具栏中某些元素的错误。 + +请参阅仓库中的 +`CHANGELOG.md `_ +获取完整的错误修复列表。 diff --git a/source/changelogs/v4.4.7.rst b/source/changelogs/v4.4.7.rst new file mode 100644 index 00000000..661f7ca5 --- /dev/null +++ b/source/changelogs/v4.4.7.rst @@ -0,0 +1,33 @@ +############# +版本 4.4.7 +############# + +发布日期:2024 年 3 月 29 日 + +**CodeIgniter4 的 4.4.7 版本发布** + +.. contents:: + :local: + :depth: 3 + +******** +安全更新 +******** + +- **Language:** 修复了 *Language 类 DoS 漏洞*。 详见 `Security advisory GHSA-39fp-mqmm-gxj6 `_。 +- **URI Security:** 添加了检查 URI 中是否包含不允许字符串的功能。此检查相当于 CodeIgniter 3 中的 URI 安全性。此功能默认为启用,但升级用户需要添加设置以启用它。详情见 :ref:`urls-uri-security`。 +- **Filters:** 修复了 Filters 处理的 URI 路径未进行 URL 解码的错误。详情见 :ref:`upgrade-447-filter-paths`。 + +******** +重大变更 +******** + +- 在以前的版本中,当使用 ``Time::difference()`` 比较日期时,如果日期包含不同于 24 小时的天数(由于夏令时导致),会返回意外结果。该错误已被修复。详情见 :ref:`Times and Dates 中的说明 `。 + +********** +修复的错误 +********** + +请参阅仓库中的 +`CHANGELOG.md `_ +获取完整的错误修复列表。 diff --git a/source/changelogs/v4.4.8.rst b/source/changelogs/v4.4.8.rst new file mode 100644 index 00000000..34dcb904 --- /dev/null +++ b/source/changelogs/v4.4.8.rst @@ -0,0 +1,26 @@ +############# +版本 4.4.8 +############# + +发布日期:2024 年 4 月 7 日 + +**CodeIgniter4 的 4.4.8 版本发布** + +.. contents:: + :local: + :depth: 3 + +******** +重大变更 +******** + +- 修复了一个导致 :doc:`Exception handler <../general/errors>` 显示与异常代码不对应的错误视图文件的错误。 + 为此,``CodeIgniter\Debug\ExceptionHandler::determineView()`` 添加了第三个参数 ``int $statusCode = 500``。 + +********** +修复的错误 +********** + +请参阅仓库中的 +`CHANGELOG.md `_ +获取完整的错误修复列表。 diff --git a/source/changelogs/v4.5.0.rst b/source/changelogs/v4.5.0.rst new file mode 100644 index 00000000..f85f8bf1 --- /dev/null +++ b/source/changelogs/v4.5.0.rst @@ -0,0 +1,404 @@ +############# +版本 4.5.0 +############# + +发布日期:2024 年 4 月 7 日 + +**CodeIgniter4 的 4.5.0 版本发布** + +.. contents:: + :local: + :depth: 3 + +********** +亮点 +********** + +- 更新最低 PHP 要求至 8.1。 +- 更新最低 PHPUnit 要求至 10.5。 +- **CORS 过滤器** (*由* `kenjis `_ *贡献*) + 详见 :doc:`../libraries/cors`。 +- 用于在生产环境中提升性能的 **spark optimize** 命令(*由* `kenjis `_ *贡献*)。 + 见 :ref:`spark_optimize`。 + +************ +增强功能 +************ + +.. _v450-required-filters: + +必需过滤器 +========== + +引入了新的 :ref:`必需过滤器 `。这些是特殊的过滤器,它们在其他种类的过滤器之前和之后应用,并且即使路由不存在也总会应用。 + +以下现有功能已重新实现为必需过滤器。 + +- :ref:`强制全局安全请求 ` +- :doc:`../general/caching` +- :ref:`性能指标 ` +- :ref:`调试工具栏 ` + +调试工具栏使用的 Benchmark **Timers** 现在收集 *必需前过滤器* 和 *必需后过滤器* 数据。 + +基准测试点已经更改: + +- 之前: + + - ``bootstrap``: 创建 Request 和 Response 对象,事件 ``pre_system``,实例化 RouteCollection 对象,加载 Routes 文件,实例化 Router 对象, + - ``routing``: 路由, +- 之后: + + - ``bootstrap``: 创建 Request 和 Response 对象,事件 ``pre_system``。 + - ``required_before_filters``: 实例化 Filters 对象,运行 *必需前过滤器*。 + - ``routing``: 实例化 RouteCollection 对象,加载 Routes 文件,实例化 Router 对象,路由, + +路由 +==== + +- **AutoRouting 改进:** 添加了 ``$translateUriToCamelCase`` 选项,允许使用驼峰式(CamelCase)控制器和方法名称。详见 :ref:`controller-translate-uri-to-camelcase`。 +- **其他改进:** + - 添加了 ``$multipleSegmentsOneParam`` 选项。启用该选项时,匹配多个段的占位符(如 ``(:any)``)将直接作为一个参数传递,即使它包含多个段。详见 :ref:`multiple-uri-segments-as-one-parameter`。 + - 现在你在 ``$override404`` 中设置的 404 控制器方法也会接收到 ``PageNotFoundException`` 消息作为第一个参数。 + - 现在你可以使用 ``__invoke()`` 方法作为默认方法。详见 :ref:`routing-default-method`。 + +命令 +==== + +- 添加了 ``spark optimize`` 命令来优化生产环境的配置。详见 :ref:`spark_optimize`。 +- 添加了 ``spark make:test`` 命令来生成测试文件的骨架。详见 :ref:`cli-generators-make-test`。 +- 添加了 ``spark config:check`` 命令来检查配置值。详见 :ref:`confirming-config-values`。 +- 添加了 ``spark phpini:check`` 命令来检查重要的 PHP ini 设置。详见 :ref:`spark-phpini-check`。 +- 添加了 ``spark lang:find`` 命令来更新翻译键。详见 :ref:`generating-translation-files-via-command`。 +- ``spark db:table`` 命令中已添加 ``--dbgroup`` 选项。详见 :ref:`Database Commands `。 + +测试 +==== + +- **DomParser:** 添加了新方法 ``seeXPath()`` 和 ``dontSeeXPath()``,允许用户使用复杂表达式直接与 DOMXPath 对象交互。 +- **CLI:** 添加了新类 ``InputOutput``,现在如果你使用 ``MockInputOutput``,可以更轻松地为命令编写测试。详见 :ref:`using-mock-input-output`。 +- **Fabricator:** Fabricator 类现在有 ``setUnique()``、``setOptional()`` 和 ``setValid()`` 方法,以允许在生成值之前对每个字段调用 Faker 的修饰符。 +- **TestResponse:** TestResponse 不再继承 ``PHPUnit\Framework\TestCase``,因为它不是一个测试。断言的返回类型现在本地化为 ``void``。 + +数据库 +====== + +查询生成器 +----------- + +.. _v450-query-builder-limit-0-behavior: + +limit(0) 行为 +^^^^^^^^^^^^^^^^^ + +- 添加了一个功能标志 ``Feature::$limitZeroAsAll`` 来修正 ``limit(0)`` 的错误行为。 +- 如果在 SQL 语句中指定了 ``LIMIT 0``,则返回 0 条记录。然而,查询生成器中存在一个错误,如果指定了 ``limit(0)``,生成的 SQL 语句将没有 ``LIMIT`` 子句,并返回所有记录。 +- 建议在 **app/Config/Feature.php** 中将 ``$limitZeroAsAll`` 设置为 ``false``,因为这个错误行为将在未来版本中修复。详见 :ref:`v450-model-findall-limit-0-behavior`。 + +其他 +------ + +- 支持包含点(``.``)的数据库名称。 + +模型 +==== + +模型字段转换 +------------------- + +添加了一个功能来将从数据库检索到的数据转换为合适的 PHP 类型。详见 :ref:`model-field-casting`。 + +.. _v450-model-findall-limit-0-behavior: + +findAll(0) 行为 +------------------- + +- 添加了一个功能标志 ``Feature::$limitZeroAsAll`` 来修正 Query Builder 的 ``limit(0)`` 的错误行为。详见 :ref:`v450-query-builder-limit-0-behavior`。 +- 如果你禁用此标志,你需要将 ``findAll(0, $offset)`` 更改为 ``findAll(null, $offset)``。 + +$updateOnlyChanged +------------------ + +添加了一个属性 ``$updateOnlyChanged``,用于决定是否仅更新 :doc:`Entity <../models/entities>` 的更改字段。如果你将此属性设置为 ``false``,当你更新一个 Entity 时,即使 Entity 中的值没有变化,也不会抛出 ``DataException`` "There is no data to update"。 + +详见 :ref:`Using CodeIgniter’s Model `。 + +保存日期 +------------ + +现在你可以配置保存 :doc:`Time <../libraries/time>` 实例时的日期/时间格式。详见 :ref:`model-saving-dates`。 + +库 +========= + +- **CORS:** 添加了 :doc:`../libraries/cors` 过滤器和类。 +- **Validation:** + - 新增规则 ``field_exists``,用于检查字段是否存在于要验证的数据中。 + - ``Validation::run()`` 的 ``$dbGroup`` 参数现在不仅接受数据库组名,还接受数据库连接实例或数据库设置数组。 +- **Session:** + - ``RedisHandler`` 现在可以配置获取锁的时间间隔(``$lockRetryInterval``)和重试次数(``$lockMaxRetries``)。 + - 现在你可以在 ``RedisHandler`` 中使用 Redis ACL(用户名和密码)。详见 :ref:`sessions-redishandler-driver`。 +- **Security:** ``Config\Security::$redirect`` 现在是特定于环境的。在生产环境中默认改为 ``true``,但在其他环境中仍然是 ``false``。 + +其他 +====== + +- **Bootstrap:** 引入了 ``CodeIgniter\Boot`` 类,取代了 **system/bootstrap.php**。 +- **Autoloader:** + - 使用 Composer 时的自动加载性能有所提升。在 **composer.json** 中在 ``autoload.psr4`` 设置中添加 ``App`` 命名空间也可能会提升应用的性能。详见 :ref:`autoloader-application-namespace`。 + - 实现了 FileLocator 缓存。详见 :ref:`file-locator-caching`。 + - 添加了 ``FileLocatorInterface``。 +- **CodeIgniter:** 新增伪变量 ``{memory_usage}``,在视图文件中显示内存使用情况,这是 CodeIgniter 3 支持的功能。 +- **Events:** 为 Spark 命令添加了事件点 ``pre_command`` 和 ``post_command``。详见 :ref:`Event Points `。 +- **HTTP:** 添加了 ``Message::addHeader()`` 方法来添加另一个具有相同名称的头。详见 :php:meth:`CodeIgniter\\HTTP\\Message::addHeader()`。 +- **Web 页面缓存:** ``ResponseCache`` 已改进,包含在缓存键中的请求 HTTP 方法。意味着如果 HTTP 方法不同,相同的 URI 将分别缓存。 +- **CSP:** 添加了 ``ContentSecurityPolicy::clearDirective()`` 方法来清除现有的 CSP 指令。详见 :ref:`csp-clear-directives`。 + +******** +重大变更 +******** + +行为更改 +======== + +小写 HTTP 方法名 +------------------ + +由于历史原因,框架使用小写的 HTTP 方法名,如 "get"、"post"。 +但方法令牌是区分大小写的,因为它可能用于具有区分大小写方法名的基于对象的系统。按照惯例,标准化方法用全大写字母 US-ASCII 字母定义。 +详见 https://www.rfc-editor.org/rfc/rfc9110#name-overview。 + +现在框架使用正确的 HTTP 方法名,如 "GET"、"POST"。 + +- ``Request::getMethod()`` 返回大写的 HTTP 方法。 +- ``CURLRequest::request()`` 不会将接受的 HTTP 方法更改为大写。 + +详情见 :ref:`upgrade-450-lowercase-http-method-name`。 + +过滤器执行顺序 +---------------------- + +控制器过滤器的执行顺序已更改。详见 :ref:`升级指南 `。 + +嵌套路由组和选项 +------------------------------- + +由于错误修复,行为已更改,使得传递给外部 ``group()`` 的选项与内部 ``group()`` 的选项合并。 +详见 :ref:`升级指南 `。 + +API\\ResponseTrait +------------------ + +现在当响应格式为 JSON 时,如果你传递字符串数据,框架将返回 JSON 响应。在以前的版本中,它返回 HTML 响应。 +详见 :ref:`升级指南 `。 + +Factories 类 +--------------- + +:doc:`../concepts/factories` 已更改为最终类(final class)。它是一个静态类,即使它被扩展,也没有替换它的方式。 + +其他 +------ + +- **AutoRouting Legacy:** 如果请求 URI 对应的控制器不存在,则改为抛出 ``PageNotFoundException``。 +- **Logger:** :php:func:`log_message()` 函数和 ``CodeIgniter\Log\Logger`` 中的 logger 方法现在不再返回 ``bool`` 值。返回类型已固定为 ``void``,以遵循 PSR-3 接口。 +- **Autoloader:** 已删除 ``FileLocator::findQualifiedNameFromPath()`` 返回的完全限定类名中的前缀 ``\``。 +- **BaseModel:** ``getIdValue()`` 方法已更改为 ``abstract``。 +- **Routing:** :ref:`404-override` 功能默认改变 Response 状态代码为 404。详见 :ref:`升级指南 `。 +- **system/bootstrap.php:** 此文件不能再使用。代码已移动到新类 ``CodeIgniter\Boot``。 + +接口更改 +======== + +.. note:: 只要你没有扩展相关的 CodeIgniter 核心类或实现这些接口,所有这些更改都是向后兼容的,不需要任何干预。 + +- **ResponseInterface:** ``ResponseInterface::setCookie()`` 的第三个参数 ``$expire`` 的默认值已从 ``''`` 修正为 ``0``。 +- **Logger:** `psr/log `_ 包已升级到 v3.0.0。 +- **Validation:** ``ValidationInterface::run()`` 的方法签名已更改。删除了 ``$dbGroup`` 参数上的 ``?string`` 类型提示。 + +.. _v450-method-signature-changes: + +方法签名更改 +============ + +设置 Cookie +------------- + +:php:func:`set_cookie()` 和 :php:meth:`CodeIgniter\\HTTP\\Response::setCookie()` 的第三个参数 ``$expire`` 已修正。 + +类型已从 ``string`` 更改为 ``int``,默认值已从 ``''`` 更改为 ``0``。 + +FileLocatorInterface +-------------------- + +- **Router:** ``RouteCollection`` 构造函数的第一个参数已从 ``FileLocator`` 更改为 ``FileLocatorInterface``。 +- **View:** ``View`` 构造函数的第三个参数已从 ``FileLocator`` 更改为 ``FileLocatorInterface``。 + +返回类型更改 +------------ + +- **Model:** ``Model`` 和 ``BaseModel`` 类中 ``objectToRawArray()`` 方法的返回类型已从 ``?array`` 更改为 ``array``。 + +传统验证规则 +--------------- + +为了在框架代码库中添加 ``declare(strict_types=1)``,所有传统验证规则类 ``CodeIgniter\Validation\FormatRules`` 和 ``CodeIgniter\Validation\Rules`` 中用于验证值的方法参数类型 ``?string`` 已移除。 + +例如,方法签名更改如下:: + + 之前:public function integer(?string $str = null): bool + 之后:public function integer($str = null): bool + +其他 +------ + +- **Logger:** 实现 PSR-3 接口的 ``CodeIgniter\Log\Logger`` 中方法的签名已修正。``bool`` 返回类型已更改为 ``void``。``$message`` 参数现在具有 ``string|Stringable`` 类型。 +- **Validation:** ``Validation::run()`` 的方法签名已更改。去掉了 ``?string`` 类型提示。 + +.. _v450-removed-deprecated-items: + +移除的弃用项 +============ + +Request +------- + +- ``RequestInterface`` 和 ``Request`` 中 ``getMethod()`` 的 ``$upper`` 参数已移除。详见 :ref:`upgrade-450-lowercase-http-method-name`。 +- ``RequestInterface`` 和 ``Request`` 中弃用的 ``isValidIP()`` 方法已移除。 +- ``IncomingRequest`` 中弃用的 ``$uri`` 和 ``$config`` 属性的可见性已更改为 protected。 +- ``IncomingRequest`` 中的 ``$enableCSRF`` 属性已移除。 +- ``IncomingRequest`` 中的 ``removeRelativeDirectory()`` 方法已移除。 +- ``Request`` 中的 ``$proxyIPs`` 属性已移除。 + +Filters +------- + +- 已移除以下弃用项,因为现在始终启用 :ref:`multiple-filters`。 + + - ``Filters::enableFilter()`` + - ``RouteCollection::getFilterForRoute()`` + - ``Router::$filterInfo`` + - ``Router::getFilter()`` + +数据库 +-------- + +- ``ModelFactory`` + +模型 +----- + +- ``BaseModel::idValue()`` +- ``BaseModel::fillPlaceholders()`` +- ``Model::idValue()`` +- ``Model::classToArray()`` + +Response +-------- + +- ``ResponseTrait::$CSP`` 属性的可见性已更改为 protected。 +- 以下弃用的属性已移除: + + - ``ResponseTrait::$CSPEnabled`` + - ``ResponseTrait::$cookiePrefix`` + - ``ResponseTrait::$cookieDomain`` + - ``ResponseTrait::$cookiePath`` + - ``ResponseTrait::$cookieSecure`` + - ``ResponseTrait::$cookieHTTPOnly`` + - ``ResponseTrait::$cookieSameSite`` + - ``ResponseTrait::$cookies`` + +Security +-------- + +- ``SecurityInterface::isExpired()`` +- ``Security::isExpired()`` +- ``Security::CSRFVerify()`` +- ``Security::getCSRFHash()`` +- ``Security::getCSRFTokenName()`` +- ``Security::sendCookie()`` +- ``Security::doSendCookie()`` + +CodeIgniter +----------- + +- ``$path`` +- ``$useSafeOutput`` +- ``useSafeOutput()`` +- ``setPath()`` + +测试 +---- + +- ``CIDatabaseTestCase`` +- ``ControllerResponse`` +- ``ControllerTester`` +- ``FeatureResponse`` +- ``FeatureTestCase`` +- ``Mock\MockSecurityConfig`` + +Spark 命令 +-------------- + +- ``migrate:create`` +- ``session:migration`` + +其他 +------ + +- **Cache:** 已移除 ``CodeIgniter\Cache\Exceptions\ExceptionInterface``。 +- **Config:** + - 已移除 ``CodeIgniter\Config\Config`` 类。 + - 已移除 ``CodeIgniter\Config\BaseService::discoverServices()`` 方法。 +- **Controller:** 已移除 ``Controller::loadHelpers()`` 方法。 +- **Exceptions:** 已移除 ``CodeIgniter\Exceptions\CastException`` 类。 +- **Entity:** 已移除 ``CodeIgniter\Entity`` 类。请使用 ``CodeIgniter\Entity\Entity``。 +- **spark:** 已移除 ``SPARKED`` 常量。 + +*************** +消息更改 +*************** + +- 添加了 ``CLI.generator.className.test`` 消息。 +- 添加了 ``Validation.field_exists`` 错误消息。 + +******* +更改 +******* + +- **Bootstrap:** **.env** 的加载和 ``ENVIRONMENT`` 的定义已移至 **bootstrap.php** 之前加载。 +- **Config:** + - ``Config\Feature::$multipleFilters`` 已移除,因为现在始终启用 :ref:`multiple-filters`。 + - 生产环境中的默认错误级别(**app/Config/Boot/production.php**)已更改为 ``E_ALL & ~E_DEPRECATED``,以匹配生产环境的默认 **php.ini**。 +- **RouteCollection:** 受保护属性 ``$routes`` 中的 HTTP 方法键已从小写修正为大写。 +- **Exceptions:** 未使用的 ``CodeIgniter\Exceptions\AlertError`` 和 ``CodeIgniter\Exceptions\EmergencyError`` 已移除。 +- **Forge:** ``SQLSRV`` Forge 现在在添加表列时将 ``ENUM`` 数据类型转换为 ``VARCHAR(n)``。在以前的版本中,它被转换为 SQL Server 中弃用的 ``TEXT``。 +- ``declare(strict_types=1)`` 已添加到大多数框架代码库。 + +************ +弃用项 +************ + +- **Services:** ``BaseService::$services`` 属性已弃用,不再使用。 +- **CodeIgniter:** + - ``determinePath()`` 方法已弃用,不再使用。 + - ``resolvePlatformExtensions()`` 方法已弃用,不再使用。它已被移到 ``CodeIgniter\Boot::checkMissingExtensions()`` 方法。 + - ``bootstrapEnvironment()`` 方法已弃用,不再使用。它已被移到 ``CodeIgniter\Boot::loadEnvironmentBootstrap()`` 方法。 + - ``initializeKint()`` 方法已弃用,不再使用。它已移到 ``Autoloader``。 + - ``autoloadKint()`` 方法已弃用,不再使用。它已移到 ``Autoloader``。 + - ``configureKint()`` 方法已弃用,不再使用。它已移到 ``Autoloader``。 +- **Response:** 构造函数参数 ``$config`` 已弃用,不再使用。 +- **Filters:** + - ``Filters`` 接受 ``Config\Filters::$methods`` 的小写 HTTP 方法键的功能已弃用。请改用正确的大写 HTTP 方法键。 + - ``spark filter:check`` 命令接受小写 HTTP 方法的功能已弃用。请改用正确的大写 HTTP 方法。 +- **RouteCollection:** ``match()`` 和 ``setHTTPVerb()`` 方法接受小写 HTTP 方法的功能已弃用。请改用正确的大写 HTTP 方法。 +- **FeatureTestTrait:** ``call()`` 和 ``withRoutes()`` 方法接受小写 HTTP 方法的功能已弃用。请改用正确的大写 HTTP 方法。 +- **Database:** ``BaseConnection::$strictOn`` 已弃用,未来将迁移到 ``MySQLi\Connection``。 + +********** +修复的错误 +********** + +请参阅仓库中的 +`CHANGELOG.md `_ +获取完整的错误修复列表。 diff --git a/source/changelogs/v4.5.1.rst b/source/changelogs/v4.5.1.rst new file mode 100644 index 00000000..68a37647 --- /dev/null +++ b/source/changelogs/v4.5.1.rst @@ -0,0 +1,19 @@ +############# +版本 4.5.1 +############# + +发布日期:2024 年 4 月 14 日 + +**CodeIgniter4 的 4.5.1 版本发布** + +.. contents:: + :local: + :depth: 3 + +********** +修复的错误 +********** + +请参阅仓库中的 +`CHANGELOG.md `_ +获取完整的错误修复列表。 diff --git a/source/cli/cli_generators.rst b/source/cli/cli_generators.rst index a339ce9f..4836c5ed 100644 --- a/source/cli/cli_generators.rst +++ b/source/cli/cli_generators.rst @@ -27,8 +27,6 @@ CodeIgniter4 现在配备了生成器,以简化常规控制器、模型、实体 .. warning:: 设置 ``--namespace`` 选项时,请确保提供的命名空间是在 ``Config\Autoload`` 中的 ``$psr4`` 数组或你的 composer 自动加载文件中定义的有效命名空间。否则,代码生成将中断。 -.. important:: 从 v4.0.5 开始,使用 ``migrate:create`` 创建迁移文件已被弃用。它将在未来版本中删除。请使用 ``make:migration`` 作为替代。另外,请使用 ``make:migration --session`` 来代替已弃用的 ``session:migration``。 - ******************* 内置生成器 ******************* @@ -212,6 +210,30 @@ make:seeder ======== * ``--namespace``:设置根命名空间。默认为 ``APP_NAMESPACE`` 的值。 * ``--suffix``:在生成的类名后附加组件后缀。 +* ``--force``:设置此标志以覆盖目标上的现有文件。 + +.. _cli-generators-make-test: + +make:test +----------- + +.. versionadded:: 4.5.0 + +创建一个新的测试文件。 + +用法: +====== +:: + + make:test [options] + +参数: +========= +* ``name``:测试类的名称。 **[必需]** + +选项: +======== +* ``--namespace``:设置根命名空间。默认为 ``Tests`` 的值。 * ``--force``:设置此标志以覆盖目标上的现有文件。 make:migration diff --git a/source/concepts/autoloader.rst b/source/concepts/autoloader.rst index 2d990455..4d858cf1 100755 --- a/source/concepts/autoloader.rst +++ b/source/concepts/autoloader.rst @@ -9,12 +9,13 @@ 每个应用程序由许多不同位置的大量类组成。 框架为核心功能提供类。你的应用程序将具有许多库、模型和其他实体才能正常工作。你可能会使用第三方类。跟踪每个文件的位置,并在一系列 ``require()`` 中硬编码该位置是一个巨大的头疼且极易出错。这就是自动加载器的用武之地。 +*********************** CodeIgniter4 自动加载器 *********************** -CodeIgniter 提供了一个非常灵活的自动加载器,可以通过非常少的配置即可使用。 -它可以定位遵循 `PSR-4 `_ -自动加载目录结构的单个命名空间类。 +CodeIgniter 提供了一个非常灵活的自动加载器,只需进行很少的配置即可使用。它可以定位符合 `PSR-4`_ 自动加载目录结构的各个命名空间类。 + +.. _PSR-4: https://www.php-fig.org/psr/psr-4/ 自动加载器本身工作良好,但也可以与其他自动加载器(如 `Composer `_)一起使用,如果需要的话,甚至可以与你自己的自定义自动加载器一起使用。 因为它们都通过 `spl_autoload_register `_ 注册,所以它们顺序工作,不会相互干扰。 @@ -24,6 +25,7 @@ CodeIgniter 提供了一个非常灵活的自动加载器,可以通过非常少 .. important:: 你应该始终小心文件名的大小写。许多开发人员在 Windows 或 macOS 上使用不区分大小写的文件系统开发。 然而,大多数服务器环境使用区分大小写的文件系统。如果文件名大小写不正确,自动加载程序无法在服务器上找到该文件。 +************* 配置 ************* @@ -34,13 +36,19 @@ CodeIgniter 提供了一个非常灵活的自动加载器,可以通过非常少 命名空间 ========== -组织类的推荐方法是为应用程序文件创建一个或多个命名空间。这对于任何业务逻辑相关的类、实体类等尤为重要。配置文件中的 ``$psr4`` 数组允许你将命名空间映射到可以找到这些类的目录: +组织类的推荐方法是为应用程序文件创建一个或多个命名空间。 + +配置文件中的 ``$psr4`` 数组允许你将命名空间映射到可以找到这些类的目录: .. literalinclude:: autoloader/001.php 每行的键是命名空间本身。这个不需要尾部反斜杠。 值是可以找到类的目录位置。 +默认情况下,命名空间 ``App`` 位于 **app** 目录中,命名空间 ``Config`` 位于 ``app/Config`` 目录中。 + +如果你在这些位置根据 `PSR-4`_ 创建类文件,自动加载器将自动加载它们。 + .. _confirming-namespaces: 确认命名空间 @@ -52,32 +60,120 @@ CodeIgniter 提供了一个非常灵活的自动加载器,可以通过非常少 php spark namespaces +.. _autoloader-application-namespace: + 应用程序命名空间 ===================== 默认情况下,应用程序目录被映射到 ``App`` 命名空间。你必须为应用程序目录中的控制器、库或模型添加命名空间,它们将在 ``App`` 命名空间下被找到。 +Config 命名空间 +---------------- + +配置文件位于 ``Config`` 命名空间中,而不是你可能预期的 ``App\Config`` 中。这使得核心系统文件即使在应用命名空间发生变化时也能始终找到它们。 + +更改应用命名空间 +---------------------- + 你可以通过编辑 **app/Config/Constants.php** 文件并在 ``APP_NAMESPACE`` 设置下设置新的命名空间值来更改此命名空间: .. literalinclude:: autoloader/002.php :lines: 2- -你需要修改引用当前命名空间的所有现有文件。 +如果你使用 Composer 自动加载器,你还需要在 **composer.json** 中更改 ``App`` 命名空间,然后运行 ``composer dump-autoload``。 -.. important:: 配置文件使用 ``Config`` 命名空间,而不是你可能期望的 ``App\Config``。这使得核心系统文件总是能够定位它们,即使应用程序命名空间已更改。 +.. code-block:: text + + { + ... + "autoload": { + "psr-4": { + "App\\": "app/" <-- Change + }, + ... + }, + ... + } + +.. note:: 自 v4.5.0 appstarter 起,``App\\`` 命名空间已被添加到 **composer.json** 的 ``autoload.psr-4`` 中。如果你的 **composer.json** 中没有此项,添加它可能会提升你应用的自动加载性能。 + +你需要修改引用当前命名空间的所有现有文件。 类映射 ======== CodeIgniter 通过不通过文件系统进行额外的 ``is_file()`` 调用来获取系统最后的性能,广泛使用类映射。你可以使用类映射链接到未使用命名空间的第三方库: +如果你使用的第三方库不是 Composer 包且没有命名空间,你可以使用类映射(classmap)来加载这些类: + .. literalinclude:: autoloader/003.php 每行的键是你要定位的类的名称。值是定位它的路径。 +**************** Composer 支持 **************** -默认情况下会自动初始化 Composer 支持。默认情况下,它会在 ``ROOTPATH . 'vendor/autoload.php'`` 查找 Composer 的自动加载文件。如果由于任何原因需要更改该文件的位置,可以修改 **app/Config/Constants.php** 中定义的值。 +默认情况下会自动初始化 Composer 支持。 + +默认情况下,它会在 ``ROOTPATH . 'vendor/autoload.php'`` 查找 Composer 的自动加载文件。如果由于任何原因需要更改该文件的位置,可以修改 **app/Config/Constants.php** 中定义的值。 + +加载器的优先级 +======================= + +如果同一个命名空间在 CodeIgniter 和 Composer 中同时定义,Composer 的自动加载器将优先尝试定位文件。 + +.. note:: 在 v4.5.0 之前,如果同一个命名空间在 CodeIgniter 和 Composer 中同时定义,CodeIgniter 的自动加载器会优先尝试定位文件。 + +.. _file-locator-caching: + +******************* +FileLocator 缓存 +******************* + +.. versionadded:: 4.5.0 + +**FileLocator** 负责查找文件或从文件中获取类名,这无法通过 PHP 自动加载来实现。 + +为了提高其性能,FileLocator 缓存已经被实现。 + +工作原理 +============ + +- 在析构时,如果缓存数据已更新,则将 FileLocator 找到的所有数据保存到缓存文件中。 +- 如果有缓存数据可用,则在实例化时恢复缓存数据。 + +缓存数据会永久使用。 + +如何删除缓存数据 +========================= + +一旦存储,缓存数据将永不过期。 + +因此,如果你添加或删除文件,或者更改现有文件路径或命名空间,旧的缓存数据将被返回,你的应用可能无法正常工作。 + +在这种情况下,你必须手动删除缓存文件。如果你通过 Composer 添加了 CodeIgniter 包,你也需要删除缓存文件。 + +你可以使用 ``spark cache:clear`` 命令: + +.. code-block:: console + + php spark cache:clear + +或者直接删除 **writable/cache/FileLocatorCache** 文件。 + +.. note:: + ``spark optimize`` 命令会清除缓存。 + +如何启用 FileLocator 缓存 +================================= + +在 **app/Config/Optimize.php** 中将以下属性设置为 ``true``: + + public bool $locatorCacheEnabled = true; + +或者你可以使用 ``spark optimize`` 命令来启用它。 -.. note:: 如果同一命名空间在 CodeIgniter 和 Composer 中都有定义,则 CodeIgniter 的自动加载器将首先获取定位该文件的机会。 +.. note:: + 此属性无法通过 + :ref:`环境变量 ` 重写。 diff --git a/source/concepts/autoloader/001.php b/source/concepts/autoloader/001.php index 68eeaddd..d6da1bce 100644 --- a/source/concepts/autoloader/001.php +++ b/source/concepts/autoloader/001.php @@ -8,8 +8,7 @@ class Autoload extends AutoloadConfig { // ... public $psr4 = [ - APP_NAMESPACE => APPPATH, // For custom app namespace - 'Config' => APPPATH . 'Config', + APP_NAMESPACE => APPPATH, ]; // ... diff --git a/source/concepts/factories.rst b/source/concepts/factories.rst index 5bfa4f8a..be95a2ad 100644 --- a/source/concepts/factories.rst +++ b/source/concepts/factories.rst @@ -262,30 +262,46 @@ setOptions 方法 或者直接删除 **writable/cache/FactoriesCache_config** 文件。 +.. note:: + 自 v4.5.0 起,``spark optimize`` 命令会清除缓存。 + 如何启用配置缓存 ============================ -取消 **public/index.php** 中以下代码的注释:: +.. versionadded:: 4.5.0 + +在 **app/Config/Optimize.php** 中将以下属性设置为 ``true``:: + + public bool $configCacheEnabled = true; + +自 v4.5.0 起,你可以使用 ``spark optimize`` 命令来启用此功能。 + +.. note:: + 此属性无法通过 + :ref:`环境变量 ` 重写。 + +.. note:: + 在 v4.5.0 之前,请在 **public/index.php** 中取消以下代码的注释:: - --- a/public/index.php - +++ b/public/index.php - @@ -49,8 +49,8 @@ if (! defined('ENVIRONMENT')) { - } + --- a/public/index.php + +++ b/public/index.php + @@ -49,8 +49,8 @@ if (! defined('ENVIRONMENT')) { + } - // Load Config Cache - -// $factoriesCache = new \CodeIgniter\Cache\FactoriesCache(); - -// $factoriesCache->load('config'); - +$factoriesCache = new \CodeIgniter\Cache\FactoriesCache(); - +$factoriesCache->load('config'); - // ^^^ Uncomment these lines if you want to use Config Caching. + // Load Config Cache + -// $factoriesCache = new \CodeIgniter\Cache\FactoriesCache(); + -// $factoriesCache->load('config'); + +$factoriesCache = new \CodeIgniter\Cache\FactoriesCache(); + +$factoriesCache->load('config'); + // ^^^ Uncomment these lines if you want to use Config Caching. - /* - @@ -79,7 +79,7 @@ $app->setContext($context); - $app->run(); + /* + @@ -79,7 +79,7 @@ $app->setContext($context); + $app->run(); - // Save Config Cache - -// $factoriesCache->save('config'); - +$factoriesCache->save('config'); - // ^^^ Uncomment this line if you want to use Config Caching. + // Save Config Cache + -// $factoriesCache->save('config'); + +$factoriesCache->save('config'); + // ^^^ Uncomment this line if you want to use Config Caching. - // Exits the application, setting the exit code for CLI-based applications + // Exits the application, setting the exit code for CLI-based applications diff --git a/source/concepts/security.rst b/source/concepts/security.rst index 89affe3c..66190efb 100755 --- a/source/concepts/security.rst +++ b/source/concepts/security.rst @@ -2,213 +2,618 @@ 安全指南 ################### -我们认真对待安全。 -CodeIgniter 集成了许多功能和技术,以强制实施良好的安全实践,或使你可以轻松地这样做。 +我们非常重视安全。 +CodeIgniter 包含许多功能和技术,以便强制执行良好的安全实践,或者让你轻松地做到这一点。 -我们尊重 `开放 Web 应用程序安全项目(OWASP) `_ -并尽可能遵循他们的建议。 +我们尊重 `开放 Web 应用安全项目 (OWASP) `_ 并尽可能地遵循他们的建议。 -以下内容来自 -`OWASP 十大安全漏洞简介 `_, -识别 Web 应用程序的十大漏洞。 -对于每个漏洞,我们提供简要描述、OWASP 建议和 CodeIgniter 的对应措施。 +以下内容来自于 +`OWASP Top Ten `_ 和 +`OWASP API Security Top 10 `_ +识别出网络应用和 API 的主要漏洞。 +我们为每个漏洞提供一个简短的描述、OWASP 的建议,然后介绍 CodeIgniter 解决这些问题的措施。 .. contents:: :local: - :depth: 1 + :depth: 2 -************ -A1 注入 -************ +***************** +OWASP Top 10 2021 +***************** -注入是通过从客户端到应用程序的输入数据不当地插入部分或全部数据。攻击向量包括 SQL、XML、ORM、代码和缓冲区溢出。 +A01:2021 访问控制失效 +============================== + +访问控制执行策略,以确保用户不能在他们预期的权限之外进行操作。失败通常会导致未经授权的信息泄露、修改或销毁所有数据,或执行超出用户限制的业务功能。 + +常见的访问控制漏洞包括: + +- 违反最小特权原则或默认拒绝原则,访问应该只授予特定功能、角色或用户,但对任何人开放。 +- 通过修改 URL(参数篡改或强制浏览)、内部应用状态或 HTML 页面,或使用攻击工具修改 API 请求来绕过访问控制检查。 +- 允许通过提供其唯一标识符来查看或编辑他人的帐户(不安全的直接对象引用)。 +- 访问缺少 POST、PUT 和 DELETE 访问控制的 API。 +- 权限提升。作为未登录的用户或作为已登录的用户时作为管理员操作。 +- 元数据操作,例如重放或篡改 JSON Web 令牌 (JWT) 访问控制令牌、或操纵 cookie 或隐藏字段以提升权限或滥用 JWT 失效。 +- CORS 配置错误允许从未经授权/未受信任的起源访问 API。 +- 强制浏览到身份验证页面的未认证用户或标准用户的特权页面。 OWASP 建议 -===================== +--------------------- -- 表示层:设置正确的内容类型、字符集和区域设置 -- 提交层:验证字段并提供反馈 -- 控制器层: sanitize 输入;使用正确的字符集进行正向输入验证 -- 模型层:参数化查询 +访问控制仅在受信任的服务器端代码或无服务器 API 中有效,在这些情况下,攻击者无法修改访问控制检查或元数据。 -CodeIgniter 对应措施 -====================== +- 除了公共资源外,一律默认拒绝。 +- 实现一次访问控制机制,并在整个应用中重用,包括最小化跨域资源共享 (CORS) 的使用。 +- 模型访问控制应执行记录所有权,而不是接受用户可以创建、读取、更新或删除任何记录的情况。 +- 应通过域模型强制执行独特的应用业务限制要求。 +- 禁用 Web 服务器目录列表,并确保文件元数据(例如 .git)和备用文件不在 Web 根目录中。 +- 记录访问控制失败,在适当时向管理员发出警报(例如,重复失败)。 +- 限制 API 和控制器访问,以最小化自动化攻击工具的危害。 +- 在注销后在服务器上使状态会话标识符失效。无状态 JWT 令牌应该是短命的,以使攻击者的机会之窗最小化。对于更长寿命的 JWT,强烈建议遵循 OAuth 标准撤销访问。 -- :ref:`invalidchars` 过滤器 +CodeIgniter 的保护措施 +---------------------- + +- :ref:`Public ` 文件夹,包括外部的应用和系统文件 - :doc:`../libraries/validation` 库 -- :doc:`HTTP 库 <../incoming/incomingrequest>` 提供了 :ref:`输入字段过滤 ` 和内容元数据的功能 +- :doc:`Security ` 库提供的 + :ref:`CSRF 保护 ` +- :doc:`../libraries/sessions` 库 +- :doc:`../libraries/throttler` 库用于限速 +- :doc:`../libraries/cors` 过滤器 +- 用于记录的 :php:func:`log_message()` 函数 +- 官方认证与授权框架 :ref:`CodeIgniter Shield ` +- 易于添加第三方认证 + +A02:2021 密码学失败 +=============================== + +首先需要确定传输和静态数据的保护需要。 例如,密码、信用卡号、健康记录、个人信息和商业机密需要额外保护,尤其是如果这些数据受到隐私法律(例如欧盟一般数据保护条例 (GDPR))或法规(例如金融数据保护,如 PCI 数据安全标准 (PCI DSS))的约束。在所有此类数据中: -********************************************* -A2 弱认证和会话管理 -********************************************* +- 有任何数据以明文传输吗?这涉及到诸如 HTTP, SMTP, FTP 等协议同样使用 TLS 升级如 STARTTLS。外部互联网流量是危险的。验证所有内部流量,例如负载均衡器、Web 服务器或后端系统之间的流量。 +- 是否存在默认使用、弱加密算法或协议,或者旧代码中使用? +- 是否使用默认加密密钥,生成或重复使用弱加密密钥,或者缺乏适当的密钥管理或轮换?密钥是否已被提交到源代码库? +- 是否没有强制加密,例如,是否缺少任何 HTTP 头(浏览器)安全指令或头? +- 服务器证书和信任链是否正确验证? +- 初始化向量是否被忽略、重复使用,还是未为加密模式生成足够安全的初始化向量?是否在使用不安全的操作模式,例如 ECB 模式?当使用认证加密更为合适时,是否仅使用加密? +- 在缺乏基于密码的密钥派生函数的情况下,是否正在使用密码作为加密密钥? +- 随机性是否用于密码学目的,但未设计为满足密码学要求?即使选择了正确的函数,是否需要开发者播种,如果没有,开发者是否覆盖了其内建的强播种功能,使用了缺乏足够熵/不可预测性的种子? +- 是否使用了如 MD5 或 SHA1 等已废弃的哈希函数,或在需要密码学哈希函数时使用了非密码学哈希函数? +- 是否正在使用如 PKCS 编号 1 v1.5 等已废弃的密码学填充方法? +- 是否存在可被利用的密码学错误消息或侧信道信息,例如填充 Oracle 攻击中的形式? -不充分的身份验证或不当的会话管理可能导致用户获得比他们有权获得的更多权限。 +OWASP 建议 +--------------------- + +至少执行以下操作,并参考相应文档: + +- 对由应用程序处理、存储或传输的数据进行分类。根据隐私法、法规要求或业务需求,确定哪些数据是敏感的。 +- 不要不必要地存储敏感数据。尽快丢弃它,或使用符合 PCI DSS 的令牌化或甚至截断。无法保留的数据不能被窃取。 +- 确保所有敏感数据在静态时都加密。 +- 确保使用最新的强标准算法、协议和密钥;使用适当的密钥管理。 +- 用安全协议(如具有前向安全性 (FS) 密码的 TLS、服务器优先的密码序列和安全参数)加密所有传输中的数据。使用诸如 HTTP 严格传输安全 (HSTS) 等指令强制加密。 +- 禁用包含敏感数据的响应缓存。 +- 根据数据分类应用所需的安全控制。 +- 不使用旧协议如 FTP 和 SMTP 传输敏感数据。 +- 使用具有工作因子(延迟因子)强自适应和加盐哈希函数来存储密码,例如 Argon2、scrypt、bcrypt 或 PBKDF2。 +- 初始化向量必须为操作模式选择合适的值。对于许多模式,这意味着使用 CSPRNG(密码学安全伪随机数生成器)。对于需要 nonce 的模式,初始化向量 (IV) 不需要 CSPRNG。在所有情况下,IV 不得在固定密钥的情况下重复使用。 +- 始终使用认证加密而不仅仅是加密。 +- 应密码学随机生成密钥,并以字节数组的形式存储在内存中。如果使用密码,则必须通过适当的密码基于密钥派生函数将其转换为密钥。 +- 确保在适当的情况下使用密码学随机性,且没有以可预测的方式或低熵例播种。大多数现代 API 不需要开发者播种 CSPRNG 以获得安全性。 +- 避免已弃用的密码学函数和填充方案,如 MD5、SHA1、PKCS 编号 1 v1.5。 +- 独立验证配置和设置的有效性。 + +CodeIgniter 的保护措施 +---------------------- + +- 全局安全访问的配置 (``Config\App::$forceGlobalSecureRequests``) +- :php:func:`force_https()` 函数 +- :doc:`../libraries/encryption` +- 数据库配置中的 :ref:`数据库配置 ` (``encrypt``) +- 官方认证与授权框架 + :ref:`CodeIgniter Shield ` + +A03:2021 注入攻击 +================== + +当应用程序存在以下情况时,容易遭受攻击: + +- 用户提供的数据没有经过应用程序验证、过滤或清理。 +- 直接在解释器中使用动态查询或没有上下文感知转义的非参数化调用。 +- 使用对象关系映射(ORM)搜索参数中的恶意数据来提取额外的敏感记录。 +- 在动态查询、命令或存储过程中的结构和恶意数据直接使用或拼接恶意数据。 + +一些常见的注入类型包括 SQL、NoSQL、操作系统命令、对象关系映射(ORM)、LDAP 和表达式语言(EL)或对象图导航库(OGNL)注入。所有解释器中的概念都是相同的。源代码审查是检测应用程序是否易受注入攻击的最佳方法。强烈建议自动化测试所有参数、头信息、URL、Cookies、JSON、SOAP 和 XML 数据输入。组织可以将静态(SAST)、动态(DAST)和交互式(IAST)应用程序安全测试工具纳入 CI/CD 管道,以在生产部署前识别引入的注入漏洞。 OWASP 建议 -===================== +--------------------- + +防止注入攻击需要将数据与命令和查询分开: -- 表示层:验证认证和角色;使用表单发送 CSRF 令牌 -- 设计:仅使用内置会话管理 -- 控制器层:验证用户、角色、CSRF 令牌 -- 模型层:验证角色 -- 提示:考虑使用请求管理器 +- 首选选项是使用安全的 API,它完全避免使用解释器、提供参数化接口或迁移到对象关系映射工具(ORM)。 -CodeIgniter 对应措施 -====================== + - 注意:即使是参数化的存储过程,如果 PL/SQL 或 T-SQL 拼接查询和数据或使用 EXECUTE IMMEDIATE 或 exec() 执行恶意数据,也可能引入 SQL 注入。 +- 使用正向服务器端输入验证。这不是一个完全的防御,因为许多应用程序需要使用特殊字符,如文本区域或移动应用程序的 API。 +- 对于任何剩余的动态查询,使用特定解释器的转义语法来转义特殊字符。 -- :doc:`会话 <../libraries/sessions>` 库 -- :doc:`安全 ` 库提供 CSRF 验证 + - 注意:无法转义 SQL 结构(如表名、列名等),因此用户提供的结构名是危险的。这在报告生成软件中是一个常见问题。 +- 在查询中使用 LIMIT 和其他 SQL 控制,以防止在 SQL 注入的情况下大规模披露记录。 + +CodeIgniter 的保护措施 +---------------------- + +- :ref:`urls-uri-security` +- :ref:`invalidchars` 过滤器 +- :doc:`../libraries/validation` 库 +- :php:func:`esc()` 函数 +- :doc:`HTTP library <../incoming/incomingrequest>` 提供 + :ref:`input field filtering ` +- 支持 :ref:`content-security-policy` +- :doc:`../database/query_builder` +- :ref:`Database escape methods ` +- :ref:`database-queries-query-bindings` + +A04:2021 不安全设计 +======================== + +不安全设计是代表不同弱点的一个广泛类别,表述为“缺失或无效的控制设计”。不安全设计不是所有其他前 10 名风险类别的来源。我们需要区分不安全设计和不安全实现,它们的根本原因和补救措施不同。 + +一个安全的设计在实现上存在缺陷,可能会导致易于被利用的漏洞。而不安全的设计,即使有完美的实现,也无法弥补其缺陷,因为从定义上说,所需的安全控制从未被创建用于防御特定攻击。不安全设计的一个因素是缺乏在开发软件或系统时内在的业务风险评估,从而未能确定需要什么级别的安全设计。 + +OWASP 建议 +--------------------- + +- 建立并使用一个安全开发生命周期,与 AppSec 专业人员合作,帮助评估和设计与安全和隐私相关的控制 +- 建立并使用一个安全设计模式库或预先准备好使用的组件 +- 使用威胁建模来针对关键的认证、访问控制、业务逻辑和关键流程 +- 将安全语言和控制集成到用户故事中 +- 在应用程序的每一层(从前端到后端)集成合理性检查 +- 编写单元和集成测试,验证所有关键流程是否能抵御威胁模型。编写每一层的用例和错误用例。 +- 根据暴露和保护需求在系统和网络层分隔层级 +- 在所有层级中通过设计来稳固地分隔租户 +- 限制用户或服务的资源消耗 + +CodeIgniter 的保护措施 +---------------------- + +- :doc:`PHPUnit testing <../testing/overview>` +- 使用 :doc:`../libraries/throttler` 进行速率限制 - 一个官方的身份验证和授权框架 :ref:`CodeIgniter Shield ` -- 易于添加第三方认证 -***************************** -A3 跨站脚本(XSS) -***************************** +A05:2021 安全配置错误 +================================== -输入验证不足,一个用户可以在网页上添加内容,当其他用户查看该网页时,这些内容可能是有害的。 +如果应用程序存在以下情况,可能会暴露于安全风险中: + +- 在应用程序堆栈的任何部分缺乏适当的安全加固,或云服务上配置不当的权限。 +- 启用了或安装了不必要的功能(例如,不必要的端口、服务、页面、账户或权限)。 +- 默认账户及其密码仍然启用且未更改。 +- 错误处理向用户透露了堆栈跟踪或其他过度详细的错误信息。 +- 对于升级系统,最新的安全功能被禁用或未安全配置。 +- 应用服务器、应用框架(如 Struts、Spring、ASP.NET)、库、数据库等的安全设置未设定为安全值。 +- 服务器没有发送安全头或指令,或它们未设定为安全值。 +- 软件过时或存在漏洞(参见 A06:2021-漏洞和过时的组件)。 + +如果没有一个集中的、可重复的应用程序安全配置过程,系统将面临更高的风险。 OWASP 建议 -===================== +--------------------- -- 表示层:根据输出上下文对所有用户数据进行输出编码;设置输入约束 -- 控制器层:正向输入验证 -- 提示:仅处理可信数据;不要在数据库中 HTML 编码存储数据 +应实施安全的安装流程,包括: -CodeIgniter 对应措施 -====================== +- 一个可重复的加固过程,使部署另一个适当锁定的环境变得快速且简单。开发、QA 和生产环境应全部按相同的方式配置,并在每个环境中使用不同的凭证。此过程应自动化,以尽量减少设置新安全环境所需的精力。 +- 一个最小的平台,没有任何不必要的功能、组件、文档和示例。删除或不安装未使用的功能和框架。 +- 将审查和更新配置作为补丁管理过程的一部分,适用于所有安全说明、更新和补丁(参见 A06:2021-漏洞和过时的组件)。审查云存储权限(例如 S3 存储桶权限)。 +- 一个分段的应用程序架构提供了组件或租户之间的有效且安全的分离,使用分段、容器化或云安全组(ACL)。 +- 向客户端发送安全指令,例如安全头信息。 +- 一个自动化过程,在所有环境中验证配置和设置的有效性。 + +CodeIgniter 的保护措施 +---------------------- + +- :ref:`spark config:check ` 命令 +- :ref:`spark phpini:check ` 命令 +- 默认情况下使用 :ref:`生产模式 ` +- :ref:`secureheaders` 过滤器 + +A06:2021 漏洞和过时的组件 +=========================================== + +当存在以下情况时,你可能会受到漏洞的影响: + +- 如果你不知道所使用的所有组件(包括客户端和服务端)的版本。这包括你直接使用的组件以及嵌套依赖项。 +- 如果软件存在漏洞、不再受支持或过期。这包括操作系统、Web/应用服务器、数据库管理系统(DBMS)、应用程序、API 以及所有组件、运行环境和库。 +- 如果你没有定期扫描漏洞并订阅与所用组件相关的安全公告。 +- 如果你没有基于风险在及时的基础上修复或升级底层平台、框架和依赖项。这在补丁管理是月度或季度任务的环境中很常见,会使组织在数天或数月内不必要地暴露于已修复的漏洞中。 +- 如果软件开发人员没有测试更新、升级或打补丁的库的兼容性。 +- 如果你没有保障组件的配置安全(参见 A05:2021-安全配置错误)。 + +OWASP 建议 +--------------------- + +应有一个补丁管理流程,以: + +- 删除未使用的依赖项、不必要的功能、组件、文件和文档。 +- 持续清点客户端和服务端组件(如框架、库)及其依赖项的版本,使用 tools like versions、OWASP Dependency Check、retire.js 等工具。持续监控如公共漏洞和暴露(CVE)和国家漏洞数据库(NVD)等来源,以查找组件中的漏洞。使用软件组成分析工具来自动化此过程。订阅电子邮件警报,以获取与所用组件相关的安全漏洞。 +- 仅从官方来源通过安全链接获取组件。优先选择签名包以减少包含已修改的恶意组件的可能(参见 A08:2021-软件和数据完整性失败)。 +- 监控未维护或不为旧版本创建安全补丁的库和组件。如果打补丁不可能,考虑部署虚拟补丁来监控、检测或防护发现的问题。 + +每个组织必须确保有一个持续的计划,用于在应用程序或组合的生命周期内监控、分类和应用更新或配置更改。 + +CodeIgniter 的保护措施 +---------------------- + +- 使用 Composer :ref:`app-starter-upgrading` + +A07:2021 身份识别和认证失败 +=================================================== + +确认用户身份、认证和 Session 管理对防范与认证相关的攻击至关重要。如果应用程序存在以下情况,可能存在认证弱点: + +- 允许自动攻击,例如凭证填充攻击,攻击者拥有一份有效的用户名和密码列表。 +- 允许暴力破解或其他自动化攻击。 +- 允许默认、弱或众所周知的密码,例如“Password1”或“admin/admin”。 +- 使用弱或无效的凭证恢复和忘记密码流程,例如不能保障安全的“基于知识的答案”。 +- 使用明文、加密或弱哈希的密码数据存储(参见 A02:2021-加密失败)。 +- 缺失或无效的多因素认证。 +- 在 URL 中暴露 Session 标识符。 +- 成功登录后重用 Session 标识符。 +- 未能正确使 Session ID 无效。用户 Session 或认证令牌(主要是单点登录(SSO)令牌)在注销或一段时间不活动期间未被正确使无效。 + +OWASP 建议 +--------------------- + +- 在可能的情况下,实现多因素认证,以防范自动化凭证填充、暴力破解和被盗凭证重复使用攻击。 +- 不要使用任何默认凭证进行运输或部署,特别是对于管理员用户。 +- 实施弱密码检查,例如针对最差的 10,000 个密码列表测试新或更改的密码。 +- 根据国家标准与技术研究所(NIST)800-63b 第 5.1.1 节的记忆密码或其他现代、基于证据的密码策略,调整密码长度、复杂性和轮换策略。 +- 确保注册、凭证恢复和 API 路径针对账户枚举攻击进行了加强,即对所有结果使用相同的消息。 +- 限制或逐渐延迟失败的登录尝试,但要小心不要创建拒绝服务情景。记录所有失败并在检测到凭证填充、暴力破解或其他攻击时警告管理员。 +- 使用服务器端的安全内置 Session 管理器,在登录后生成高熵的新随机 Session ID。Session 标识符不应在 URL 中出现,应安全存储,并在注销、空闲和绝对超时后使其失效。 + +CodeIgniter 的保护措施 +---------------------- + +- :doc:`Session <../libraries/sessions>` 库 +- 官方的认证和授权框架 :ref:`CodeIgniter Shield ` + +A08:2021 软件和数据完整性失败 +============================================= + +软件和数据完整性失败涉及未能保护代码和基础设施免受完整性违反的影响。例如,当应用程序依赖来自不受信任来源、仓库和内容分发网络(CDNs)的插件、库或模块时,就可能存在问题。不安全的 CI/CD 管道可能引入未经授权的访问、恶意代码或系统泄露的潜在风险。 + +最后,许多应用程序现在包括自动更新功能,而这些更新在没有充分的完整性验证的情况下下载并应用于先前受信任的应用程序。攻击者可能会上传他们自己的更新,并分发和运行在所有安装中。 + +另一个例子是,当对象或数据被编码或序列化为攻击者可以看到和修改的结构时,存在不安全的反序列化风险。 + +OWASP 建议 +--------------------- + +- 使用数字签名或类似机制来验证软件或数据是否来自预期来源,并且未被修改。 +- 确保库和依赖项(如 npm 或 Maven)使用受信任的仓库。如果你有更高的风险配置文件,请考虑托管一个经过验证的内部已知良好仓库。 +- 确保使用软件供应链安全工具(如 OWASP Dependency Check 或 OWASP CycloneDX)来验证组件不包含已知漏洞。 +- 确保对代码和配置更改进行审查过程,以尽量减少引入恶意代码或配置到软件管道中的可能性。 +- 确保你的 CI/CD 管道具有适当的隔离、配置和访问控制,以确保代码在构建和部署过程中的完整性。 +- 确保未签名或未加密的序列化数据未发送到不受信任的客户端,而不进行某种形式的完整性检查或数字签名,以检测序列化数据的篡改或重放。 + +CodeIgniter 的保护措施 +---------------------- + +- 不适用 + +A09:2021 安全日志记录和监控失败 +================================================= + +此类别旨在帮助检测、升级和响应活动中的入侵。如果没有日志记录和监控,入侵将无法被检测到。以下情况下发生日志记录、检测、监控和主动响应不足的情况: + +- 可审计事件(如登录、登录失败和高价值交易)未被记录。 +- 警告和错误没有生成、生成不充分或不清晰的日志消息。 +- 未监控应用程序和 API 的日志以发现可疑活动。 +- 日志仅本地存储。 +- 适当的警报阈值和响应升级过程没有到位或无效。 +- 动态应用安全测试(DAST)工具(如 OWASP ZAP)的渗透测试和扫描未触发警报。 +- 应用程序无法实时或接近实时检测、升级或警报活跃攻击。 + +如果将日志记录和警报事件暴露给用户或攻击者(参见 A01:2021-访问控制失效),你就可能会受到信息泄露的影响。 + +OWASP 建议 +--------------------- + +开发人员应根据应用程序的风险实施以下一些或所有控制: + +- 确保所有登录、访问控制和服务器端输入验证失败事件都能够记录下来,并具有足够的用户上下文以识别可疑或恶意账户,并保存足够长的时间以允许延迟的法证分析。 +- 确保生成的日志格式易于日志管理解决方案消费。 +- 确保日志数据正确编码,以防止对日志记录或监控系统的注入或攻击。 +- 确保高价值交易有完整性控制的审计追踪,以防止篡改或删除,如只追加数据库表或类似机制。 +- DevSecOps 团队应建立有效的监控和警报系统,以便快速检测和响应可疑活动。 +- 建立或采用事故响应和恢复计划,如国家标准与技术研究所(NIST)800-61r2 或更新版本。 + +存在商业和开源的应用保护框架,如 OWASP ModSecurity Core Rule Set,以及开源日志关联软件,如 Elasticsearch、Logstash、Kibana(ELK)堆栈,具有自定义仪表板和警报功能。 + +CodeIgniter 的保护措施 +---------------------- + +- :doc:`Logging <../general/logging>` 库 +- 官方的认证和授权框架 :ref:`CodeIgniter Shield ` + +A10:2021 服务器端请求伪造(SSRF) +=========================================== + +当一个 Web 应用程序在获取远程资源时未验证用户提供的 URL,就会发生 SSRF 漏洞。它允许攻击者强迫应用程序将构造的请求发送到意料之外的目的地,即使受到防火墙、VPN 或其他类型的网络访问控制列表(ACL)的保护。 + +由于现代 Web 应用程序为最终用户提供了便利的功能,获取 URL 成为了常见的情况。因此,SSRF 的发生率在增加。与此同时,由于云服务和架构的复杂性,SSRF 的严重性也在增加。 + +OWASP 建议 +--------------------- + +开发人员可以通过实施以下一些或所有的深度防御控制来预防 SSRF: + +从网络层: + +- 将远程资源访问功能划分到不同的网络中,以减少 SSRF 的影响 +- 强制执行“默认拒绝”的防火墙策略或网络访问控制规则,以阻止所有不重要的内部网络流量。 + + - 提示: + + * 基于应用程序建立防火墙规则的所有权和生命周期。 + * 记录防火墙上所有接受和阻止的网络流(参见 A09:2021-安全日志记录和监控失败)。 + +从应用层: + +- 清理和验证所有客户端提供的输入数据 +- 通过允许列表强制 URL 模式、端口和目的地 +- 不要将原始响应发送给客户端 +- 禁用 HTTP 重定向 +- 注意 URL 一致性,以避免 DNS 重新绑定和“检查时与使用时”(TOCTOU) 竞争条件等攻击 + +不要通过使用拒绝列表或正则表达式来缓解 SSRF。攻击者拥有绕过拒绝列表的有效负载列表、工具和技能。 + +CodeIgniter 的保护措施 +---------------------- -- :php:func:`esc()` 函数 - :doc:`../libraries/validation` 库 -- 支持 :ref:`content-security-policy` +- :doc:`HTTP 库 <../incoming/incomingrequest>` 提供了 :ref:`输入字段过滤 ` + +****************************** +OWASP API Security Top 10 2023 +****************************** + +API1:2023 对象级授权失效 +=========================================== + +API 往往会暴露处理对象标识符的端点,这样会产生广泛的对象级访问控制问题。在每个使用来自用户的 ID 访问数据源的函数中,都应考虑对象级授权检查。 + +OWASP 建议 +--------------------- + +- 实施依赖于用户策略和层级的适当授权机制。 +- 使用授权机制检查登录用户是否有权在每个使用客户端输入访问数据库记录的函数中执行请求的操作。 +- 优先使用随机且不可预测的值作为记录 ID 的 GUID。 +- 编写测试以评估授权机制的漏洞。不要部署使测试失败的更改。 + +CodeIgniter 的保护措施 +---------------------- + +- 官方的认证和授权框架 :ref:`CodeIgniter Shield ` +- :doc:`PHPUnit 测试 <../testing/overview>` -*********************************** -A4 不安全的直接对象引用 -*********************************** +API2:2023 认证失效 +=============================== -不安全的直接对象引用发生在应用程序根据用户提供的输入直接提供对对象的访问时。由于这个漏洞, -攻击者可以绕过授权并直接访问系统中的资源,例如数据库记录或文件。 +认证机制常常被错误地实现,使得攻击者能够破解认证令牌或利用实现缺陷临时或永久地假冒其他用户的身份。破坏系统识别客户端/用户的能力就会破坏 API 的整体安全性。 OWASP 建议 -===================== +--------------------- -- 表示层:不公开内部数据;使用随机引用映射 -- 控制器层:从可信源或随机引用映射获取数据 -- 模型层:在更新数据之前验证用户角色 +- 确保了解所有可能的 API 认证流程(移动应用/Web/实现单击认证的深层链接等)。询问你的工程师是否遗漏了某些流程。 +- 了解你的认证机制。确保理解它们的用途和使用方法。OAuth 不是认证,API 密钥也不是。 +- 不要在认证、令牌生成或密码存储方面重新发明轮子。使用标准。 +- 凭据恢复/忘记密码端点应在防御暴力破解、速率限制和锁定保护方面与登录端点同等对待。 +- 对于敏感操作(例如更改账户所有者的电子邮件地址/两因素认证电话号码),需要重新认证。 +- 使用 OWASP 认证备忘清单。 +- 尽可能实施多因素认证。 +- 实施防暴力破解机制,以减轻凭据填充、字典攻击和针对认证端点的暴力破解攻击。此机制应比 API 上的常规速率限制机制更严格。 +- 实施账号锁定/验证码机制,以防止针对特定用户的暴力破解攻击。实施弱密码检查。 +- API 密钥不应用于用户认证。它们仅应用于 API 客户端认证。 -CodeIgniter 对应措施 -====================== +CodeIgniter 的保护措施 +---------------------- + +- :doc:`../incoming/filters` +- :ref:`routing-spark-routes` 命令 +- 官方的认证和授权框架 :ref:`CodeIgniter Shield ` +- 用于速率限制的 :doc:`../libraries/throttler` + +API3:2023 对象属性级授权失效 +==================================================== + +这一类别结合了 API3:2019 的过度数据暴露和 API6:2019 的大量赋值问题,集中在根本原因:缺乏或不当的对象属性级授权验证。这导致了未经授权方的信息暴露或篡改。 + +OWASP 建议 +--------------------- + +- 当通过 API 端点暴露一个对象时,始终确保用户应该访问你暴露的对象属性。 +- 避免使用诸如 to_json() 和 to_string() 之类的通用方法。相反,应选择具体的对象属性进行返回。 +- 如果可能,避免使用自动将客户端输入绑定到代码变量、内部对象或对象属性的功能(“大量赋值”)。 +- 仅允许客户端更新对象的特定属性。 +- 实施基于模式的响应验证机制作为额外的安全层。作为该机制的一部分,定义并强制执行所有 API 方法返回的数据。 +- 根据端点的业务/功能需求,将返回的数据结构保持在最低限度。 + +CodeIgniter 的保护措施 +---------------------- + +- 模型的 :ref:`model-allowed-fields` +- 官方的认证和授权框架 :ref:`CodeIgniter Shield ` + +API4:2023 不受限制的资源消耗 +=========================================== + +满足 API 请求需要诸如网络带宽、CPU、内存和存储等资源。其他诸如电子邮件/SMS/电话或生物信息验证等资源由服务提供商通过 API 集成提供,并按请求付费。成功的攻击可能导致拒绝服务或运营成本增加。 + +OWASP 建议 +--------------------- + +- 使用一种可以轻松限制内存、CPU、重启次数、文件描述符和进程(如容器/无服务器代码,例如 Lambdas)的方法。 +- 定义并强制执行所有传入参数和负载数据的最大尺寸,例如字符串的最大长度、数组中的最大元素数和最大上传文件大小(无论存储在本地还是云存储中)。 +- 实施限制客户端在定义时间范围内与 API 交互频率的机制(速率限制)。 +- 速率限制应根据业务需求进行微调。某些 API 端点可能需要更严格的策略。 +- 限制/节流单个 API 客户端/用户执行单个操作的次数或频率(例如验证一次性密码,或在不访问一次性 URL 的情况下请求密码恢复)。 +- 添加适当的服务器端验证以控制查询字符串和请求体参数,尤其是那些控制响应中返回记录数量的参数。 +- 为所有服务提供商/API 集成配置消费限制。如果无法设置消费限制,应配置帐单警报。 + +CodeIgniter 的保护措施 +---------------------- - :doc:`../libraries/validation` 库 -- 一个官方的身份验证和授权框架 :ref:`CodeIgniter Shield ` -- 易于添加第三方认证 +- 用于速率限制的 :doc:`../libraries/throttler` -**************************** -A5 安全配置不当 -**************************** +API5:2023 功能级授权失效 +============================================= -应用程序架构的不正确配置可能导致错误,这些错误可能危及整个架构的安全。 +复杂的访问控制策略,包括不同的层级、组和角色,以及行政和常规功能之间不明确的分离,往往导致授权缺陷。通过利用这些问题,攻击者可以访问其他用户的资源和/或管理功能。 OWASP 建议 -===================== +--------------------- -- 表示层:增强 Web 和应用服务器;使用 HTTP 严格传输安全 -- 控制器层:增强 Web 和应用服务器;保护 XML 堆栈 -- 模型层:增强数据库服务器 +你的应用程序应有一个一致且易于分析的授权模块,该模块应从所有业务功能中调用。通常,这种保护是由应用代码外部的一个或多个组件提供的。 -CodeIgniter 对应措施 -====================== +- 执行机制应该默认拒绝所有访问,需要对每个功能的访问显式授予特定角色。 +- 根据功能级授权缺陷审查你的 API 端点,同时牢记应用程序的业务逻辑和组层次结构。 +- 确保所有管理控制器都继承自实现基于用户组/角色的授权检查的管理抽象控制器。 +- 确保常规控制器内的管理功能实现基于用户组和角色的授权检查。 -- 引导期间的正常检查 +CodeIgniter 的保护措施 +---------------------- -************************** -A6 敏感数据暴露 -************************** +- :doc:`../incoming/filters` +- 官方的认证和授权框架 :ref:`CodeIgniter Shield ` -敏感数据在通过网络传输时必须受到保护。此类数据可以包括用户凭据和信用卡。经验法则是,如果存储的数据必须受到保护,那么在传输过程中也必须受到保护。 +API6:2023 不受限制访问敏感业务流程 +========================================================= + +存在这种风险的 API 暴露了某些业务流程——例如购票或发布评论——而没有考虑到这些功能如果以自动化的方式被过度使用会对业务造成怎样的损害。这并不一定来源于实现上的漏洞。 OWASP 建议 -===================== +--------------------- -- 表示层:使用 TLS 1.2;使用强密码和散列;不要将密钥或散列发送到浏览器 -- 控制器层:使用强密码和散列 -- 模型层:强制与服务器进行加密通信 +缓解计划应该分两层进行: -CodeIgniter 对应措施 -====================== +- 业务层:识别出如果过度使用可能对业务造成损害的业务流程。 +- 工程层:选择合适的保护机制来减轻业务风险。 -- 全局安全访问的配置(``Config\App::$forceGlobalSecureRequests``) -- :php:func:`force_https()` 函数 -- :doc:`../libraries/encryption` -- :ref:`数据库配置 ` (``encrypt``) +一些保护机制相对简单,而另一些则更复杂。以下方法通常用于减缓自动化威胁: + +- 设备指纹识别:拒绝意外客户端设备的服务(例如无头浏览器)往往会使威胁行为者使用更复杂的解决方案,从而增加其成本。 +- 人类检测:使用验证码或更先进的生物识别解决方案(例如输入模式)。 +- 非人类模式检测:分析用户流程以检测非人类模式(例如用户在不到一秒钟内访问了“添加到购物车”和“完成购买”功能)。 +- 考虑阻止 Tor 出站节点和知名代理的 IP 地址。 -**************************************** -A7 缺少功能级访问控制 -**************************************** +保护并限制直接由机器使用的 API(如开发者和 B2B API)的访问。这些 API 往往是攻击者的容易目标,因为它们通常没有实现所有必要的保护机制。 -敏感数据在通过网络传输时必须受到保护。此类数据可以包括用户凭据和信用卡。经验法则是,如果存储的数据必须受到保护,那么在传输过程中也必须受到保护。 +CodeIgniter 的保护措施 +---------------------- + +- 不适用 + +API7:2023 服务端请求伪造 +===================================== + +服务端请求伪造(SSRF)漏洞可能会在 API 获取远程资源时未对用户提供的 URI 进行验证时出现。这使得攻击者能够迫使应用程序向意外的目标发送伪造的请求,即使这些目标受防火墙或 VPN 保护。 OWASP 建议 -===================== +--------------------- -- 表示层:确保非 Web 数据在 Web 根目录之外;验证用户和角色;发送 CSRF 令牌 -- 控制器层:验证用户和角色;验证 CSRF 令牌 -- 模型层:验证角色 +- 在你的网络中隔离资源获取机制:通常这些功能是为了获取远程资源而不是内部资源。 +- 在可能的情况下,使用白名单: -CodeIgniter 对应措施 -====================== + - 用户预计下载资源的远程来源(例如 Google Drive, Gravatar 等) + - URL 方案和端口 + - 给定功能的可接受媒体类型 +- 禁用 HTTP 重定向。 +- 使用经过良好测试和维护的 URL 解析器,以避免由于 URL 解析不一致引起的问题。 +- 验证并清理所有客户端提供的输入数据。 +- 不要向客户端发送原始响应。 -- :ref:`Public ` 文件夹,其中应用程序和系统位于外部 -- :doc:`安全库 ` 提供了 :ref:`CSRF 验证 ` 的功能 +CodeIgniter 的保护措施 +---------------------- -************************************ -A8 跨站请求伪造(CSRF) -************************************ +- :doc:`../libraries/validation` 库 +- :doc:`HTTP library <../incoming/incomingrequest>` 提供 :ref:`输入字段过滤 ` +- :doc:`CURLRequest <../libraries/curlrequest>` 类 +- :doc:`URI <../libraries/uri>` 类 -CSRF 是一种攻击,它强制最终用户在其当前已认证的 Web 应用程序上执行不需要的操作。 +API8:2023 安全配置错误 +=================================== + +API 及其支持系统通常包含复杂的配置,旨在使 API 更加可定制。软件和 DevOps 工程师可能会忽视这些配置,或在配置时未遵循安全最佳实践,从而为各种类型的攻击打开了大门。 OWASP 建议 -===================== +--------------------- + +API 生命周期应包括: -- 表示层:验证用户和角色;发送 CSRF 令牌 -- 控制器层:验证用户和角色;验证 CSRF 令牌 -- 模型层:验证角色 +- 一个可重复的强化过程,以快速轻松地部署一个适当锁定的环境。 +- 在整个 API 栈中审查和更新配置的任务。审查应包含:编排文件、API 组件和云服务(例如 S3 存储桶权限)。 +- 一个自动化过程,持续评估所有环境中配置和设置的有效性。 -CodeIgniter 对应措施 -====================== +此外: -- :doc:`安全库 ` 提供了 :ref:`CSRF 验证 ` 的功能 +- 确保所有从客户端到 API 服务器及任何上下游组件的 API 通信都在加密通信通道(TLS)上进行,无论它是内部 API 还是公开 API。 +- 明确每个 API 可以访问的 HTTP 动词:应禁用所有其他 HTTP 动词(例如 HEAD)。 +- 预计从基于浏览器的客户端(例如,Web 应用前端)访问的 API 应至少: -********************************************** -A9 使用已知漏洞的组件 -********************************************** + - 实施适当的跨域资源共享(CORS)策略 + - 包含适用的安全头 +- 将传入内容类型/数据格式限制为符合业务/功能要求的那些。 +- 确保 HTTP 服务器链中的所有服务器(例如,负载均衡器、反向和正向代理、后端服务器)以一致的方式处理传入请求,以避免反序列化问题。 +- 在适用的情况下,定义并强制执行所有 API 响应负载模式,包括错误响应,以防止异常跟踪和其他有价值的信息被返回给攻击者。 -许多应用程序都有已知的漏洞和已知的攻击策略,可被利用以获得远程控制或利用数据。 +CodeIgniter 的保护措施 +---------------------- + +- 全局安全访问配置(``Config\App::$forceGlobalSecureRequests``) +- :php:func:`force_https()` 函数 +- :ref:`已定义路由 ` +- :ref:`auto-routing-improved` +- :doc:`../libraries/cors` 过滤器 + +API9:2023 不当的库存管理 +======================================= + +相比传统的 web 应用程序,API 往往暴露更多的端点,因此适当且更新的文档变得尤为重要。适当的主机和已部署 API 版本的库存管理也很重要,可以减轻诸如弃用 API 版本和暴露调试端点等问题。 OWASP 建议 -===================== +--------------------- -- 不要使用这些 +- 清点所有 API 主机并记录每个主机的重要方面,关注 API 环境(例如生产、暂存、测试、开发),谁应具有对主机的网络访问权限(例如公共、内部、合作伙伴)以及 API 版本。 +- 清点集成的服务并记录其重要方面,例如它们在系统中的角色、交换的数据(数据流)及其敏感性。 +- 记录 API 的所有方面,例如认证、错误、重定向、速率限制、跨域资源共享(CORS)策略和端点,包括它们的参数、请求和响应。 +- 通过采用开放标准自动生成文档。在你的 CI/CD 流水线中包含文档构建。 +- 仅向授权使用 API 的人提供 API 文档。 +- 对所有暴露的 API 版本使用外部保护措施(例如专门的 API 安全解决方案),而不仅限于当前的生产版本。 +- 避免在非生产 API 部署中使用生产数据。如果无法避免,这些端点应得到与生产端点相同的安全处理。 +- 当新版本的 API 包含安全改进时,进行风险分析,以通知旧版本所需的缓解措施。例如,是否可以在不破坏 API 兼容性的情况下向后移植改进,或需要迅速移除旧版本并强迫所有客户端迁移到最新版本。 -CodeIgniter 对应措施 -====================== +CodeIgniter 的保护措施 +---------------------- -- 必须审查纳入的第三方库 +- :ref:`routing-spark-routes` 命令 -************************************** -A10 未验证的重定向和转发 -************************************** +API10:2023 不安全的 API 消费 +===================================== -缺陷的业务逻辑或注入的可执行代码可能会不当地重定向用户。 +开发者往往比起用户输入更信任从第三方 API 接收的数据,因此更倾向于采用较弱的安全标准。为了攻击 API,攻击者更可能是针对集成的第三方服务,而不是直接尝试攻击目标 API。 OWASP 建议 -===================== +--------------------- -- 表示层:不要使用 URL 重定向;使用随机间接引用 -- 控制器层:不要使用 URL 重定向;使用随机间接引用 -- 模型层:验证角色 +- 在评估服务提供商时,评估其 API 的安全状态。 +- 确保所有 API 交互都通过安全通信通道(TLS)进行。 +- 在使用从集成 API 接收到的数据之前,始终验证并适当清理这些数据。 +- 保持一个集成 API 可能重定向到的已知位置白名单:不要盲目跟随重定向。 -CodeIgniter 对应措施 -====================== +CodeIgniter 的保护措施 +---------------------- -- :doc:`HTTP 库 <../incoming/incomingrequest>` 提供... -- :doc:`Session 库 <../libraries/sessions>` 提供 :ref:`sessions-flashdata` +- :doc:`CURLRequest <../libraries/curlrequest>` 类 +- :doc:`../libraries/validation` 库 diff --git a/source/conf.py b/source/conf.py index 7a0f888d..c1e9dfdd 100755 --- a/source/conf.py +++ b/source/conf.py @@ -23,10 +23,10 @@ copyright = '2019-' + str(year_now) + ' CodeIgniter 基金会' # The short X.Y version. -version = '4.4' +version = '4.5' # The full version, including alpha/beta/rc tags. -release = '4.4.5' +release = '4.5.1' # -- General configuration --------------------------------------------------- diff --git a/source/database/configuration.rst b/source/database/configuration.rst index 3d82e27d..ff2ea253 100755 --- a/source/database/configuration.rst +++ b/source/database/configuration.rst @@ -110,7 +110,7 @@ DSN .. _database-config-explanation-of-values: ********************** -值的解释: +值的解释 ********************** ================ =========================================================================================================== @@ -122,7 +122,8 @@ DSN **password** 用于连接数据库的密码。(``SQLite3`` 不使用此密码) **database** 要连接的数据库名称。 - .. note:: CodeIgniter 不支持数据库、表格和列名称中的点(``.``)。 + .. note:: CodeIgniter 不支持在表名和列名中使用点 (``.``)。 + 从 v4.5.0 版本开始,支持带点的数据库名。 **DBDriver** 数据库驱动名称。驱动名称区分大小写。 你可以设置完全限定的类名以使用自定义驱动。 支持的驱动:``MySQLi``、``Postgre``、``SQLite3``、``SQLSRV`` 和 ``OCI8``。 @@ -130,28 +131,60 @@ DSN **pConnect** true/false (布尔值)- 是否使用持久连接。 **DBDebug** true/false (布尔值)- 数据库错误发生时是否抛出异常。 **charset** 与数据库通信使用的字符集。 -**DBCollat** 与数据库通信使用的字符整理(``MySQLi`` 仅)。 +**DBCollat** (仅限 ``MySQLi``)与数据库通信时使用的字符集。 **swapPre** 一个默认的表前缀,应该与 ``DBPrefix`` 互换。这对于分布式应用程序很有用,在那里你可能运行手动编写的查询,并需要最终用户仍可自定义前缀。 -**schema** 数据库模式,默认值因驱动而异。(``Postgre`` 和 ``SQLSRV`` 使用。) -**encrypt** 是否使用加密连接。 - ``SQLSRV`` 驱动程序接受 true/false - ``MySQLi`` 驱动程序接受带有以下选项的数组: - * ``ssl_key`` - 私钥文件的路径 - * ``ssl_cert`` - 公钥证书文件的路径 - * ``ssl_ca`` - 证书颁发机构文件的路径 - * ``ssl_capath`` - 包含 PEM 格式可信 CA 证书的目录的路径 - * ``ssl_cipher`` - 用冒号 (``:``) 分隔的允许使用的加密的列表 - * ``ssl_verify`` - true/false; 是否验证服务器证书 (``MySQLi`` 仅) -**compress** 是否使用客户端压缩(``MySQLi`` 仅)。 -**strictOn** true/false (布尔值)- 是否强制“严格模式”连接,有助于开发应用程序时确保严格的 SQL (``MySQLi`` 仅)。 +**schema** (仅限 ``Postgre`` 和 ``SQLSRV``)数据库模式,默认值因驱动而异。 +**encrypt** (仅限 ``MySQLi`` 和 ``SQLSRV``)是否使用加密连接。 + 有关 ``MySQLi`` 设置,请参见 :ref:`MySQLi encrypt `。 + ``SQLSRV`` 驱动程序接受 true/false。 +**compress** (仅限 ``MySQLi``)是否使用客户端压缩。 +**strictOn** (仅限 ``MySQLi``)true/false(布尔值)- 是否强制使用“严格模式”连接,有助于确保在开发应用程序时使用严格的 SQL。 **port** 数据库端口号 - 默认端口为空字符串 ``''`` (或使用 ``SQLSRV`` 动态端口)。 -**foreignKeys** true/false (布尔值)- 是否启用外键约束(``SQLite3`` 仅)。 +**foreignKeys** (仅限 ``SQLite3``)true/false (布尔值)- 是否启用外键约束。 .. important:: SQLite3 外键约束默认关闭。 请参阅 `SQLite 文档 `_。 要实施外键约束,请将此配置项设置为 true。 -**busyTimeout** 毫秒(int) - 表锁定时休眠指定时间(``SQLite3`` 仅)。 -**numberNative** true/false (布尔值) - 是否启用 MYSQLI_OPT_INT_AND_FLOAT_NATIVE(仅适用于 ``MySQLi``)。 +**busyTimeout** (仅限 ``SQLite3``)毫秒(int)- 当表被锁定时,休眠指定时间。 +**numberNative** (仅限 ``MySQLi``)true/false(布尔值)- 是否启用 MYSQLI_OPT_INT_AND_FLOAT_NATIVE。 +**dateFormat** 默认的日期/时间格式,如 PHP 的 `DateTime format`_。 + * ``date`` - 日期格式 + * ``datetime`` - 日期和时间格式 + * ``datetime-ms`` - 带毫秒的日期和时间格式 + * ``datetime-us`` - 带微秒的日期和时间格式 + * ``time`` - 时间格式 + 这一功能可以从 v4.5.0 版本开始使用,你可以通过例如 ``$db->dateFormat['datetime']`` 来获取值。 + 当前,数据库驱动程序不会直接使用这些值,但 :ref:`Model ` 会使用它们。 ================ =========================================================================================================== +.. _DateTime format: https://www.php.net/manual/en/datetime.format.php + .. note:: 根据你使用的数据库驱动程序(``MySQLi``、``Postgre`` 等),并非所有的值都是必需的。例如,在使用 ``SQLite3`` 时,你不需要提供用户名或密码,数据库名称将是数据库文件的路径。 + +MySQLi +====== + +hostname +-------- + +配置一个 Socket 连接 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +要通过文件系统套接字连接到 MySQL 服务器,应在 ``'hostname'`` 设置中指定套接字的路径。CodeIgniter 的 MySQLi 驱动程序会注意到这一点并正确配置连接。 + +.. literalinclude:: configuration/011.php + :lines: 11-18 + +.. _mysqli-encrypt: + +encrypt +------- + +MySQLi 驱动程序接受包含以下选项的数组: + +* ``ssl_key`` - 私钥文件的路径 +* ``ssl_cert`` - 公钥证书文件的路径 +* ``ssl_ca`` - 证书颁发机构文件的路径 +* ``ssl_capath`` - 包含以 PEM 格式存储的可信 CA 证书的目录路径 +* ``ssl_cipher`` - 允许用于加密的密码列表,用冒号(``:``)分隔 +* ``ssl_verify`` - true/false; 是否验证服务器证书 diff --git a/source/database/configuration/001.php b/source/database/configuration/001.php index b4336abb..874c673f 100644 --- a/source/database/configuration/001.php +++ b/source/database/configuration/001.php @@ -18,8 +18,8 @@ class Database extends Config 'DBPrefix' => '', 'pConnect' => false, 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', + 'charset' => 'utf8mb4', + 'DBCollat' => 'utf8mb4_general_ci', 'swapPre' => '', 'encrypt' => false, 'compress' => false, diff --git a/source/database/configuration/004.php b/source/database/configuration/004.php index 4b779617..58243f8a 100644 --- a/source/database/configuration/004.php +++ b/source/database/configuration/004.php @@ -10,7 +10,7 @@ class Database extends Config // MySQLi public array $default = [ - 'DSN' => 'MySQLi://username:password@hostname:3306/database?charset=utf8&DBCollat=utf8_general_ci', + 'DSN' => 'MySQLi://username:password@hostname:3306/database?charset=utf8mb4&DBCollat=utf8mb4_general_ci', // ... ]; diff --git a/source/database/configuration/005.php b/source/database/configuration/005.php index ca23aea1..34f64e38 100644 --- a/source/database/configuration/005.php +++ b/source/database/configuration/005.php @@ -20,8 +20,8 @@ class Database extends Config 'DBPrefix' => '', 'pConnect' => true, 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', + 'charset' => 'utf8mb4', + 'DBCollat' => 'utf8mb4_general_ci', 'swapPre' => '', 'encrypt' => false, 'compress' => false, @@ -36,8 +36,8 @@ class Database extends Config 'DBPrefix' => '', 'pConnect' => true, 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', + 'charset' => 'utf8mb4', + 'DBCollat' => 'utf8mb4_general_ci', 'swapPre' => '', 'encrypt' => false, 'compress' => false, diff --git a/source/database/configuration/006.php b/source/database/configuration/006.php index 1cd5fafe..ff633992 100644 --- a/source/database/configuration/006.php +++ b/source/database/configuration/006.php @@ -18,8 +18,8 @@ class Database extends Config 'DBPrefix' => '', 'pConnect' => true, 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', + 'charset' => 'utf8mb4', + 'DBCollat' => 'utf8mb4_general_ci', 'swapPre' => '', 'compress' => false, 'encrypt' => false, diff --git a/source/database/configuration/010.php b/source/database/configuration/010.php index de06d231..d1f89b31 100644 --- a/source/database/configuration/010.php +++ b/source/database/configuration/010.php @@ -10,7 +10,7 @@ class Database extends Config // Postgre public array $default = [ - 'DSN' => 'Postgre://username:password@hostname:5432/database?charset=utf8&connect_timeout=5&sslmode=1', + 'DSN' => 'Postgre://username:password@hostname:5432/database?charset=utf8&connect_timeout=5&sslmode=require', // ... ]; diff --git a/source/database/configuration/011.php b/source/database/configuration/011.php new file mode 100644 index 00000000..01a0e876 --- /dev/null +++ b/source/database/configuration/011.php @@ -0,0 +1,21 @@ + '/cloudsql/toolbox-tests:europe-north1:toolbox-db', + // ... + 'DBDriver' => 'MySQLi', + // ... + ]; + + // ... +} diff --git a/source/database/connecting/006.php b/source/database/connecting/006.php index 55e80107..6804ad8a 100644 --- a/source/database/connecting/006.php +++ b/source/database/connecting/006.php @@ -10,8 +10,8 @@ 'DBPrefix' => '', 'pConnect' => false, 'DBDebug' => true, - 'charset' => 'utf8', - 'DBCollat' => 'utf8_general_ci', + 'charset' => 'utf8mb4', + 'DBCollat' => 'utf8mb4_general_ci', 'swapPre' => '', 'encrypt' => false, 'compress' => false, diff --git a/source/database/events.rst b/source/database/events.rst index 50300b99..b64415f3 100755 --- a/source/database/events.rst +++ b/source/database/events.rst @@ -12,11 +12,15 @@ 事件 ********** +.. _database-events-dbquery: + DBQuery ======= -无论成功与否,每当有新的查询被执行时,此事件就会被触发。唯一的参数是当前查询的 :doc:`查询 ` 实例。你可以使用这个来在 STDOUT 显示所有的查询,或者记录到文件,甚至创建工具来进行自动查询分析,帮助你发现可能丢失的索引、慢查询等。 +此事件在每次运行新查询时触发,无论成功与否。唯一的参数是当前查询的 :doc:`Query ` 实例。 + +你可以使用此事件将所有查询显示在 STDOUT,或记录到文件,甚至创建工具进行自动查询分析,以帮助你发现潜在的缺失索引、慢查询等。 -示例用法可能是: +记录所有查询的示例: .. literalinclude:: events/001.php diff --git a/source/database/examples.rst b/source/database/examples.rst index cabe33ea..839c63b5 100755 --- a/source/database/examples.rst +++ b/source/database/examples.rst @@ -4,7 +4,7 @@ 以下页面包含显示数据库类用法的示例代码。有关完整详细信息,请阅读描述每个函数的单独页面。 -.. note:: CodeIgniter 不支持数据库、表格和列名称中的点(``.``)。 +.. note:: CodeIgniter 不支持在表名和列名中使用点(``.``)。自 v4.5.0 起,支持带点的数据库名称。 .. contents:: :local: diff --git a/source/database/metadata.rst b/source/database/metadata.rst index 6453aada..ce109c1e 100755 --- a/source/database/metadata.rst +++ b/source/database/metadata.rst @@ -48,13 +48,13 @@ $db->getFieldNames() 返回包含字段名称的数组。可以通过两种方式调用此查询: -1. 你可以提供表格名称并从 ``$db->object`` 调用它: +1. 你可以提供表格名称并从 ``$db`` 对象调用它: - .. literalinclude:: metadata/003.php + .. literalinclude:: metadata/003.php 2. 你可以通过从查询结果对象调用函数来收集与任何查询关联的字段名称: -.. literalinclude:: metadata/004.php + .. literalinclude:: metadata/004.php 确定表中是否存在字段 ========================================== @@ -86,18 +86,25 @@ $db->getFieldData() .. literalinclude:: metadata/006.php -如果你已经运行了一个查询,你可以使用结果对象而不是提供表格名称: +如果你的数据库支持,下列数据可以通过此函数获取: -.. literalinclude:: metadata/007.php +- ``name`` - 列名称 +- ``type`` - 列的类型 +- ``max_length`` - 列的最大长度 +- ``nullable`` - 如果列允许为空,则为布尔值 ``true`` ,否则为布尔值 ``false`` +- ``default`` - 默认值 +- ``primary_key`` - 如果列是主键,则为整数 ``1``(即使有多个主键,所有主键值都是整数 ``1``),否则为整数 ``0``(此字段目前仅对 ``MySQLi`` 和 ``SQLite3`` 可用) + +.. note:: 自 v4.4.0 起,SQLSRV 支持 ``nullable``。 + +$query->getFieldData() +---------------------- -如果数据库支持,可以从此函数获取以下数据: +如果你已经运行了一个查询,可以使用结果对象而不是提供表名: + +.. literalinclude:: metadata/007.php -- name - 列名称 -- type - 列的类型 -- max_length - 列的最大长度 -- primary_key - 如果列是主键,则为整数 ``1`` (即使有多个主键,也全部为整数 ``1``),否则为整数 ``0`` (此字段当前仅适用于 MySQL 和 SQLite3) -- nullable - 如果列可为空,则为布尔值 ``true``,否则为布尔值 ``false`` -- default - 默认值 +.. note:: 返回的数据与 ``$db->getFieldData()`` 返回的数据不同。如果你无法获取所需的数据,请使用 ``$db->getFieldData()``。 列出表中的索引 =========================== diff --git a/source/database/queries.rst b/source/database/queries.rst index 7cda03be..c88ecb24 100755 --- a/source/database/queries.rst +++ b/source/database/queries.rst @@ -10,7 +10,7 @@ 查询基础知识 ************ -.. note:: CodeIgniter 不支持数据库、表格和列名称中的点(``.``)。 +.. note:: CodeIgniter 不支持在表名和列名中使用点(``.``)。自 v4.5.0 起,支持带点的数据库名称。 常规查询 =============== @@ -90,6 +90,7 @@ $db->protectIdentifiers() .. literalinclude:: queries/008.php +.. _database-queries-escaping: *************** 转义值 @@ -123,6 +124,8 @@ $db->protectIdentifiers() .. important:: ``escapeLikeString()`` 方法使用 ``'!'`` (感叹号)来转义 ``LIKE`` 条件的特殊字符。因为此方法转义了你自己要用引号括起来的部分字符串,所以它无法自动为你添加 ``ESCAPE '!'`` 条件,因此你必须手动完成这一操作。 +.. _database-queries-query-bindings: + ************** 查询绑定 ************** diff --git a/source/database/query_builder.rst b/source/database/query_builder.rst index 6b2c3969..5027f3ad 100755 --- a/source/database/query_builder.rst +++ b/source/database/query_builder.rst @@ -7,7 +7,7 @@ CodeIgniter 不要求每个数据库表都有自己的类文件。它提供了 除了简单性之外,使用查询构建器功能的一个主要好处是,它允许你创建数据库独立的应用程序,因为查询语法是由每个数据库适配器生成的。它也允许进行更安全的查询,因为系统会自动对值进行转义。 -.. note:: CodeIgniter 不支持数据库、表名和列名中使用点(``.``)。 +.. note:: CodeIgniter 不支持在表名和列名中使用点(``.``)。自 v4.5.0 起,支持带点的数据库名称。 .. contents:: :local: diff --git a/source/database/results/016.php b/source/database/results/016.php index f6d0e232..b3356729 100644 --- a/source/database/results/016.php +++ b/source/database/results/016.php @@ -1,3 +1,3 @@ getCustomRowObject(0, \App\Entities\User::class); +$row = $query->getRow(0, \App\Entities\User::class); diff --git a/source/database/transactions.rst b/source/database/transactions.rst index 2a437969..ea112383 100755 --- a/source/database/transactions.rst +++ b/source/database/transactions.rst @@ -43,10 +43,10 @@ CodeIgniter 使用一种非常类似于流行数据库类 ADODB 的方法来处 管理错误 =============== -当你在 **app/Config/Database.php** 文件中设置 ``DBDebug`` 为 true 时,如果查询错误发生, -所有查询都将回滚,并抛出异常。所以你会看到一个标准的错误页面。 +.. note:: + 自 v4.3.0 起,即使 ``DBDebug`` 为 true,在事务期间默认也不会抛出异常。 -如果 ``DBDebug`` 为 false,你可以像这样管理自己的错误: +你可以像这样管理自己的错误: .. literalinclude:: transactions/003.php diff --git a/source/dbmgmt/db_commands.rst b/source/dbmgmt/db_commands.rst index 92de5b7f..dee49c35 100644 --- a/source/dbmgmt/db_commands.rst +++ b/source/dbmgmt/db_commands.rst @@ -24,7 +24,24 @@ db:table --show php spark db:table --show -使用此命令时,假定表存在。否则,CodeIgniter 将抱怨数据库没有表。 +使用此命令时,假设数据库中已存在表。 +否则,CodeIgniter 会提示数据库中没有表。 + +.. _db-command-specify-the-dbgroup: + +指定数据库组 +========================== + +db:table --dbgroup +------------------ + +.. versionadded:: 4.5.0 + +你可以使用 ``--dbgroup`` 选项来指定使用的数据库组: + +.. code-block:: console + + php spark db:table --show --dbgroup tests 检索一些记录 ===================== diff --git a/source/dbmgmt/forge.rst b/source/dbmgmt/forge.rst index 54ee6ca9..f9eb1a4d 100644 --- a/source/dbmgmt/forge.rst +++ b/source/dbmgmt/forge.rst @@ -135,6 +135,14 @@ TEXT SQLSRV 上不应使用 ``TEXT``,它已被弃用。 欲知详情,请参见 `ntext, text, 和 image (Transact-SQL) - SQL Server | Microsoft Learn `_。 +ENUM +^^^^ + +并非所有数据库都支持 ``ENUM``。 + +从 v4.5.0 开始,``SQLSRV`` Forge 会将 ``ENUM`` 数据类型转换为 ``VARCHAR(n)``。 +之前的版本转换为 ``TEXT``。 + .. _forge-addfield-default-value-rawsql: 作为默认值的原始 SQL 字符串 diff --git a/source/dbmgmt/forge/016.php b/source/dbmgmt/forge/016.php index e8db91a9..4fb2abfc 100644 --- a/source/dbmgmt/forge/016.php +++ b/source/dbmgmt/forge/016.php @@ -2,4 +2,4 @@ $attributes = ['ENGINE' => 'InnoDB']; $forge->createTable('table_name', false, $attributes); -// produces: CREATE TABLE `table_name` (...) ENGINE = InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci +// produces: CREATE TABLE `table_name` (...) ENGINE = InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci diff --git a/source/dbmgmt/migration.rst b/source/dbmgmt/migration.rst index 7b1bc3f3..1e0ebdfe 100644 --- a/source/dbmgmt/migration.rst +++ b/source/dbmgmt/migration.rst @@ -66,6 +66,7 @@ 例如,假设我们在 Autoload 配置文件中定义了以下命名空间: .. literalinclude:: migration/004.php + :lines: 2- 这将查找 **APPPATH/Database/Migrations** 和 **ROOTPATH/MyCompany/Database/Migrations** 中的任何迁移。这使得在你的可重用、模块化代码套件中包含迁移变得很简单。 diff --git a/source/dbmgmt/migration/004.php b/source/dbmgmt/migration/004.php index 1e4c831c..24c68408 100644 --- a/source/dbmgmt/migration/004.php +++ b/source/dbmgmt/migration/004.php @@ -1,6 +1,6 @@ APPPATH, - 'MyCompany' => ROOTPATH . 'MyCompany', + APP_NAMESPACE => APPPATH, + 'MyCompany' => ROOTPATH . 'MyCompany', ]; diff --git a/source/extending/events.rst b/source/extending/events.rst index cde4b756..fdbd4341 100644 --- a/source/extending/events.rst +++ b/source/extending/events.rst @@ -69,11 +69,30 @@ Events 库也使你可以在自己的代码中简单地创建事件。要使用 事件挂钩点 ============ -以下是 CodeIgniter 核心代码中可用的事件挂钩点列表: +用于 Web 应用 +------------- -* **pre_system** 在系统执行的早期调用。URI、请求和响应已经实例化,但尚未进行页面缓存检查、路由和执行“before”控制器过滤器。 -* **post_controller_constructor** 在控制器实例化后但在任何方法调用发生前立即调用。 -* **post_system** 在系统执行结束后,在最终渲染的页面发送到浏览器之前调用,在执行“after”控制器过滤器之后。 -* **email** 从 ``CodeIgniter\Email\Email`` 成功发送邮件后调用。接收 ``Email`` 类属性数组作为参数。 +以下是由 **public/index.php** 触发的可用事件挂钩点列表: + +* **pre_system** 在系统执行早期调用。URI、Request 和 Response 已经实例化,但页面缓存检查、路由和“before”控制器过滤器的执行尚未发生。 +* **post_controller_constructor** 在控制器实例化后立即调用,但在任何方法调用发生之前。 +* **post_system** 在系统执行结束时、在执行“after”控制器过滤器之后、最终渲染的页面发送到浏览器之前调用。 + +.. _event-points-for-cli-apps: + +用于 CLI 应用 +------------- + +以下是 :doc:`../cli/spark_commands` 触发的可用事件点列表: + +* **pre_command** 在命令代码执行之前调用。 +* **post_command** 在命令代码执行之后调用。 + +其他 +------ + +以下是每个库可用的事件点列表: + +* **email** 在 ``CodeIgniter\Email\Email`` 成功发送电子邮件之后调用。接收一个包含 ``Email`` 类属性的数组作为参数。 * **DBQuery** 在数据库查询成功或失败后调用。接收 ``Query`` 对象。 -* **migrate** 在对 ``latest()`` 或 ``regress()`` 的成功迁移调用后调用。接收 ``MigrationRunner`` 的当前属性以及方法名称。 +* **migrate** 在成功调用 ``latest()`` 或 ``regress()`` 进行迁移后调用。接收当前 ``MigrationRunner`` 属性以及方法名称。 diff --git a/source/general/caching.rst b/source/general/caching.rst index c8dcab29..35fb9d27 100755 --- a/source/general/caching.rst +++ b/source/general/caching.rst @@ -13,7 +13,13 @@ CodeIgniter 允许你缓存网页以达到最大性能。 缓存是如何工作的? ====================== -可以按页面启用缓存,并设置页面在刷新之前应保持缓存的时间长度。当首次加载页面时,将使用当前配置的缓存引擎对页面进行缓存。在后续的页面加载中,将检索缓存并发送给浏览器。如果缓存已过期,将在发送给浏览器之前删除并刷新。 +缓存可以基于每个页面进行启用,你可以设置页面在刷新之前应保持缓存的时间长度。 + +.. note:: 基于每页意味着基于每个 URI。从 v4.5.0 开始,请求的 HTTP 方法也被考虑在内。这意味着如果 HTTP 方法不同,相同的 URI 将分别被缓存。 + +当页面第一次加载时,页面将使用当前配置的缓存引擎进行缓存。在后续的页面加载中,缓存将被检索并发送到请求用户的浏览器。 + +如果缓存已过期,它将被删除并在发送到浏览器之前刷新。 .. note:: Benchmark 标签不会被缓存,所以启用缓存后你仍可以查看页面加载速度。 diff --git a/source/general/common_functions.rst b/source/general/common_functions.rst index 6da4f41f..7f492937 100755 --- a/source/general/common_functions.rst +++ b/source/general/common_functions.rst @@ -178,7 +178,7 @@ CodeIgniter 提供了一些全局定义的函数和变量,在任何时候都可 .. literalinclude:: common_functions/004.php - 有关更多详细信息,请参阅 :doc:`视图 ` 页面。 + 有关更多详细信息,请参阅 :doc:`视图 <../outgoing/views>` 和 :doc:`../outgoing/view_renderer` 页面。 .. php:function:: view_cell($library[, $params = null[, $ttl = 0[, $cacheName = null]]]) @@ -297,12 +297,14 @@ CodeIgniter 提供了一些全局定义的函数和变量,在任何时候都可 :param string $level: 严重级别 :param string $message: 要记录的消息 :param array $context: 应在 $message 中替换的标签及其值的关联数组 - :returns: 如果记录成功则为 true,如果记录有问题则为 false + :returns: void :rtype: bool + .. note:: 自 v4.5.0 起,返回值被固定为兼容 PSR Log。在以前的版本中,如果日志记录成功则返回 ``true``,如果有问题则返回 ``false``。 + 使用 **app/Config/Logger.php** 中定义的日志处理程序记录消息。 - 级别可以是以下值之一:**emergency**、**alert**、**critical**、**error**、**warning**、**notice**、**info** 或 **debug**。 + 日志级别可以是以下值之一:``emergency``、``alert``、``critical``、``error``、``warning``、``notice``、``info`` 或 ``debug``。 上下文可以用来在消息字符串中替换值。有关完整详细信息,请参阅 :doc:`日志记录信息 ` 页面。 @@ -351,7 +353,7 @@ CodeIgniter 提供了一些全局定义的函数和变量,在任何时候都可 :returns: 路由路径(基于 baseURL 的 URI 相对路径) :rtype: string - .. note:: 此函数要求控制器/方法必须在 **app/Config/routes.php** 中定义路由。 + .. note:: 此函数要求控制器/方法必须在 **app/Config/Routes.php** 中定义路由。 .. important:: ``route_to()`` 返回一个 *路由* 路径,而不是站点的完整 URI 路径。如果你的 **baseURL** 包含子文件夹,返回值与链接的 URI 并不相同。在这种情况下,请改用 :php:func:`url_to()`。另请参阅 :ref:`urls-url-structure`。 diff --git a/source/general/configuration.rst b/source/general/configuration.rst index 306249c3..d9dda252 100755 --- a/source/general/configuration.rst +++ b/source/general/configuration.rst @@ -10,6 +10,15 @@ :local: :depth: 2 +什么是配置类? +******************************* + +配置类用于定义系统默认配置值。系统配置值通常是*静态*的。配置类旨在保留配置应用程序操作方式的设置,而不是响应每个用户的个别设置。 + +不建议在配置类实例化后,在执行期间修改值。换句话说,建议将配置类视为不可变或只读的类。这尤其重要,如果你使用 :ref:`factories-config-caching`。 + +配置值可以在类文件中硬编码,也可以在实例化时从环境变量中获取。 + 使用配置文件 ********************************** @@ -151,6 +160,8 @@ CodeIgniter 期望 **.env** 文件与 **app** 目录一起位于项目的根目 app_forceGlobalSecureRequests = true app_CSPEnabled = true +.. _configuration-classes-and-environment-variables: + 配置类和环境变量 *********************************************** @@ -185,14 +196,16 @@ CodeIgniter 期望 **.env** 文件与 **app** 目录一起位于项目的根目 .. _env-var-replacements-for-data: -环境变量作为数据的替换 +作为数据的环境变量 ============================================== -务必要始终记住,在 **.env** 中的环境变量 **仅用于替换现有数据**。 +务必要始终记住,你的 **.env** 文件中的环境变量 **只是现有标量值的替代**。 -简单地说,你只能通过在 **.env** 中设置来更改 Config 类中存在的属性的值。 +简单来说,你只能通过在 **.env** 文件中设置来更改 Config 类中存在的属性的标量值。 -你不能添加 Config 类中未定义的属性,也不能将其更改为数组(如果定义的属性的值是标量)。 + 1. 你不能添加 Config 类中未定义的属性。 + 2. 你不能将属性中的标量值更改为数组。 + 3. 你不能向现有数组中添加元素。 例如,你不能只是在 **.env** 中放置 ``app.myNewConfig = foo`` 并期望你的 ``Config\App`` 在运行时神奇地拥有该属性和值。 @@ -290,3 +303,52 @@ CodeIgniter 期望 **.env** 文件与 **app** 目录一起位于项目的根目 通过上面的示例,在实例化 ``MySalesConfig`` 时,它将最终具有声明的三个属性,但 ``$target`` 属性的值将通过将 ``RegionalSales`` 视为“注册器”来覆盖。生成的配置属性: .. literalinclude:: configuration/011.php + +.. _confirming-config-values: + +确认配置值 +************************ + +实际的 Config 对象属性值在运行时由 :ref:`registrars`、:ref:`环境变量 ` 和 :ref:`factories-config-caching` 进行更改。 + +CodeIgniter 有以下 :doc:`命令 <../cli/spark_commands>` 来检查实际的配置值。 + +.. _spark-config-check: + +config:check +============ + +.. versionadded:: 4.5.0 + +例如,如果你想检查 ``Config\App`` 实例: + +.. code-block:: console + + php spark config:check App + +输出结果如下: + +.. code-block:: none + + Config\App#6 (12) ( + public 'baseURL' -> string (22) "http://localhost:8080/" + public 'allowedHostnames' -> array (0) [] + public 'indexPage' -> string (9) "index.php" + public 'uriProtocol' -> string (11) "REQUEST_URI" + public 'defaultLocale' -> string (2) "en" + public 'negotiateLocale' -> boolean false + public 'supportedLocales' -> array (1) [ + 0 => string (2) "en" + ] + public 'appTimezone' -> string (3) "UTC" + public 'charset' -> string (5) "UTF-8" + public 'forceGlobalSecureRequests' -> boolean false + public 'proxyIPs' -> array (0) [] + public 'CSPEnabled' -> boolean false + ) + + Config Caching: Disabled + +你可以看到配置缓存是否已启用。 + +.. note:: 如果启用了配置缓存,则始终使用缓存的值。有关详情,请参阅 :ref:`factories-config-caching`。 diff --git a/source/general/errors.rst b/source/general/errors.rst index 3b3b3695..958d97e4 100755 --- a/source/general/errors.rst +++ b/source/general/errors.rst @@ -2,8 +2,8 @@ 错误处理 ############## -CodeIgniter 通过 Exception 在你的系统中内置了错误报告,包括 -`SPL 集合 `_,以及框架提供的一些 Exception。 +CodeIgniter 通过“异常(Exception)”在你的系统中内置了错误报告,包括 +`SPL 集合 `_,以及框架提供的一些“异常”。 取决于你的环境设置,当抛出错误或异常时的默认操作是显示详细的错误报告,除非应用程序在 ``production`` 环境下运行。 在 ``production`` 环境中,会显示更通用的消息以对用户保持最佳体验。 @@ -12,22 +12,31 @@ CodeIgniter 通过 Exception 在你的系统中内置了错误报告,包括 :local: :depth: 2 -使用 Exception +使用异常 ================ -本节简要概述了对 Exception 不太了解的新程序员或开发人员的情况。 +本节简要概述了对异常不太了解的新程序员或开发人员的情况。 -Exception 简单来说就是在抛出异常时发生的事件。这将中止脚本的当前流程,然后执行将转移到错误处理程序,后者将显示适当的错误页面: +什么是异常 +------------------ + +异常简单来说就是在抛出异常时发生的事件。这将中止脚本的当前流程,然后执行将转移到错误处理程序,后者将显示适当的错误页面: .. literalinclude:: errors/001.php +捕获异常 +------------------- + 如果你正在调用可能抛出异常的方法,你可以使用 ``try/catch`` 块捕获该异常: .. literalinclude:: errors/002.php 如果 ``$userModel`` 抛出异常,则会捕获它并执行 catch 块中的代码。在这个例子中,脚本终止,并回显 ``UserModel`` 定义的错误信息。 -在上面的示例中,我们捕获任何类型的 Exception。如果我们只想监视特定类型的异常,如 ``UnknownFileException``,我们可以在 catch 参数中指定它。任何其他抛出的不属于捕获的异常子类的异常都将传递给错误处理程序: +捕获特定异常 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +在上面的示例中,我们捕获任何类型的异常。如果我们只想监视特定类型的异常,如 ``DataException``,我们可以在 catch 参数中指定它。任何其他抛出的不属于捕获的异常子类的异常都将传递给错误处理程序: .. literalinclude:: errors/003.php @@ -51,10 +60,10 @@ Exception 简单来说就是在抛出异常时发生的事件。这将中止脚 .. warning:: 请注意,**.env** 文件中的设置会添加到 ``$_SERVER`` 和 ``$_ENV`` 中。作为副作用,这意味着如果显示详细的错误报告,**你的安全凭据将被公开**。 -记录 Exception +记录异常 ------------------ -默认情况下,除了 404 - 页面未找到异常之外的所有异常都会记录日志。这可以通过设置 **app/Config/Exceptions.php** 的 ``$log`` 值来打开和关闭: +默认情况下,除了“404 - Page Not Found”异常之外的所有异常都会记录日志。这可以通过设置 **app/Config/Exceptions.php** 的 ``$log`` 值来打开和关闭: .. literalinclude:: errors/005.php @@ -62,9 +71,34 @@ Exception 简单来说就是在抛出异常时发生的事件。这将中止脚 .. literalinclude:: errors/006.php -.. note:: 如果你当前的日志设置没有配置记录 **关键** 错误,则仍可能不会为 exception 记录日志,因为所有 exception 都记录为关键错误。 +.. note:: 如果你的当前 :ref:`日志配置 ` 没有设置记录 ``critical`` 错误的话,异常仍然可能不会被记录,因为所有的异常都作为 ``critical`` 错误来记录。 + +.. _logging_deprecation_warnings: + +弃用警告日志 +------------ + +.. versionadded:: 4.3.0 + +默认情况下,所有由 ``error_reporting()`` 报告的错误都会作为 ``ErrorException`` 对象抛出。这包括 ``E_DEPRECATED`` 和 ``E_USER_DEPRECATED`` 错误。随着 PHP 8.1+ 的使用激增,许多用户可能会看到由于 `passing null to non-nullable arguments of internal functions `_ 抛出的异常。为了简化向 PHP 8.1 的迁移,你可以指示 CodeIgniter 记录这些弃用警告,而不是抛出它们。 + +首先,确保你的 ``Config\Exceptions`` 副本已更新了两个新属性并设置如下: + +.. literalinclude:: errors/012.php + +接下来,根据你在 ``Config\Exceptions::$deprecationLogLevel`` 中设置的日志级别,检查 ``Config\Logger::$threshold`` 中定义的日志门槛是否涵盖了弃用日志级别。如果没有,请相应调整。 + +.. literalinclude:: errors/013.php + +之后,后续的弃用警告将会被记录而不是抛出。 + +此功能也适用于用户弃用警告: -框架 Exception +.. literalinclude:: errors/014.php + +对于测试你的应用程序,你可能希望总是抛出弃用警告。你可以通过将环境变量 ``CODEIGNITER_SCREAM_DEPRECATIONS`` 设置为真值来配置这一点。 + +框架异常 ==================== 以下框架异常可用: @@ -72,12 +106,16 @@ Exception 简单来说就是在抛出异常时发生的事件。这将中止脚 PageNotFoundException --------------------- -这用于表示 404 页面未找到错误。抛出时,系统将显示在 **app/Views/errors/html/error_404.php** 中找到的视图。你应该自定义站点的所有错误视图。如果在 **app/Config/Routes.php** 中指定了 404 覆盖页面,则会调用它而不是标准的 404 页面: +这用于表示 404,页面未找到错误: .. literalinclude:: errors/007.php 你可以传入一个消息到异常中,它将显示在404页面上的默认消息位置: +有关默认的 404 视图文件位置,请参见 :ref:`http-status-code-and-error-views`。 + +如果你在 **app/Config/Routing.php** 或 **app/Config/Routes.php** 中指定了 :ref:`404-override`,那么将会调用这个覆盖页面,而不是标准的 404 页面。 + ConfigException --------------- @@ -121,46 +159,39 @@ RedirectException .. versionadded:: 4.3.0 从 v4.3.0 开始,你可以为异常类指定 HTTP 状态码来实现 -``HTTPExceptionInterface``。 +``CodeIgniter\Exceptions\HTTPExceptionInterface``。 当 CodeIgniter 的异常处理程序捕获实现了 ``HTTPExceptionInterface`` 的异常时,异常代码将成为 HTTP 状态码。 -.. _error-specify-exit-code: - -在异常中指定退出代码 -=================================== - -.. versionadded:: 4.3.0 - -从 v4.3.0 开始,你可以为异常类指定退出代码来实现 -``HasExitCodeInterface``。 +.. _http-status-code-and-error-views: -当 CodeIgniter 的异常处理程序捕获实现了 ``HasExitCodeInterface`` 的异常时, ``getExitCode()`` 方法返回的代码将成为退出代码。 +HTTP 状态码和错误视图 +========================= -.. _logging_deprecation_warnings: +异常处理程序会显示对应于 HTTP 状态码的错误视图(如果存在的话)。 -记录弃用警告 -============================ +例如,``PageNotFoundException`` 实现了 ``HTTPExceptionInterface``,所以它的异常代码 ``404`` 将成为 HTTP 状态码。因此,如果它被抛出,系统将在处理网页请求时显示 **app/Views/errors/html** 文件夹中的 **error_404.php**。如果是通过 CLI 调用,系统将显示 **app/Views/errors/cli** 文件夹中的 **error_404.php**。 -.. versionadded:: 4.3.0 +如果没有与 HTTP 状态码对应的视图文件,那么将显示 **production.php** 或 **error_exception.php**。 -默认情况下, ``error_reporting()`` 报告的所有错误都会作为 ``ErrorException`` 对象抛出。这些错误包括 ``E_DEPRECATED`` 和 ``E_USER_DEPRECATED`` 错误。随着 PHP 8.1+ 的大规模使用,许多用户可能会看到由于 `向内部函数的非空参数传递 null `_ 抛出的异常。为了方便迁移到 PHP 8.1,你可以指示 CodeIgniter 记录弃用而不是抛出它们。 +.. note:: 如果在 PHP INI 配置中开启了 ``display_errors``,将选择 **error_exception.php** 并显示详细的错误报告。 -首先,确保你的 ``Config\Exceptions`` 副本已更新,并设置如下: +你应该自定义 **app/Views/errors/html** 文件夹中的所有错误视图以适应你的站点。 -.. literalinclude:: errors/012.php +你还可以为特定的 HTTP 状态码创建错误视图。例如,如果你想创建一个 "400 Bad Request" 的错误视图,添加 **error_400.php**。 -接下来,根据在 ``Config\Exceptions::$deprecationLogLevel`` 中设置的日志级别,检查在 ``Config\Logger::$threshold`` 中定义的记录器阈值是否涵盖了弃用日志级别。如果没有,请相应调整它。 +.. warning:: 如果存在对应 HTTP 状态码的错误视图文件,异常处理程序将无论环境如何显示该文件。视图文件必须自行实现,不在生产环境中显示详细的错误信息。 -.. literalinclude:: errors/013.php +.. _error-specify-exit-code: -之后,后续的弃用将被记录而不是抛出。 +在你的异常中指定退出代码 +============================= -此功能也适用于用户弃用: +.. versionadded:: 4.3.0 -.. literalinclude:: errors/014.php +自 v4.3.0 起,你可以为你的异常类指定退出代码,以实现 ``CodeIgniter\Exceptions\HasExitCodeInterface``。 -为了测试你的应用程序,你可能希望始终在弃用时抛出。你可以通过将环境变量 ``CODEIGNITER_SCREAM_DEPRECATIONS`` 设置为真值来配置此行为。 +当实现了 ``HasExitCodeInterface`` 的异常被 CodeIgniter 的异常处理程序捕获时,从 ``getExitCode()`` 方法返回的代码将成为退出代码。 .. _custom-exception-handlers: diff --git a/source/general/errors/003.php b/source/general/errors/003.php index 2d230d34..07418eef 100644 --- a/source/general/errors/003.php +++ b/source/general/errors/003.php @@ -1,7 +1,9 @@ find($id); -} catch (\CodeIgniter\UnknownFileException $e) { +} catch (DataException $e) { // do something here... } diff --git a/source/general/errors/004.php b/source/general/errors/004.php index 752b23bd..4b816ece 100644 --- a/source/general/errors/004.php +++ b/source/general/errors/004.php @@ -1,8 +1,10 @@ find($id); -} catch (\CodeIgniter\UnknownFileException $e) { +} catch (DataException $e) { // do something here... throw new \RuntimeException($e->getMessage(), $e->getCode(), $e); diff --git a/source/general/errors/005.php b/source/general/errors/005.php index 54a3276f..152a0813 100644 --- a/source/general/errors/005.php +++ b/source/general/errors/005.php @@ -6,5 +6,7 @@ class Exceptions extends BaseConfig { - public $log = true; + // ... + public bool $log = true; + // ... } diff --git a/source/general/errors/006.php b/source/general/errors/006.php index 9d9bb636..614d61e1 100644 --- a/source/general/errors/006.php +++ b/source/general/errors/006.php @@ -6,5 +6,7 @@ class Exceptions extends BaseConfig { - public $ignoredCodes = [404]; + // ... + public array $ignoreCodes = [404]; + // ... } diff --git a/source/general/errors/007.php b/source/general/errors/007.php index a06e5000..585f4aa6 100644 --- a/source/general/errors/007.php +++ b/source/general/errors/007.php @@ -1,5 +1,9 @@ find($id)) { - throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); +use CodeIgniter\Exceptions\PageNotFoundException; + +$page = $pageModel->find($id); + +if ($page === null) { + throw PageNotFoundException::forPageNotFound(); } diff --git a/source/general/managing_apps.rst b/source/general/managing_apps.rst index 24410fa6..4429c5ab 100755 --- a/source/general/managing_apps.rst +++ b/source/general/managing_apps.rst @@ -42,6 +42,8 @@ .. literalinclude:: managing_apps/003.php +.. _running-multiple-app: + 使用一个 CodeIgniter 安装运行多个应用程序 =============================================================== diff --git a/source/general/modules/001.php b/source/general/modules/001.php index 1f93e213..dcc1fbe3 100644 --- a/source/general/modules/001.php +++ b/source/general/modules/001.php @@ -6,9 +6,9 @@ class Autoload extends AutoloadConfig { + // ... public $psr4 = [ - APP_NAMESPACE => APPPATH, // For custom namespace - 'Config' => APPPATH . 'Config', + APP_NAMESPACE => APPPATH, 'Acme\Blog' => ROOTPATH . 'acme/Blog', ]; diff --git a/source/general/urls.rst b/source/general/urls.rst index dc691efb..ca6aa641 100755 --- a/source/general/urls.rst +++ b/source/general/urls.rst @@ -55,6 +55,45 @@ URI 路径 /ci-blog/blog/news/2022/10 查询 page=2 ========== ==================================== ========================================= +.. _urls-uri-security: + +URI 安全性 +========== + +.. versionadded:: 4.4.7 + +.. important:: + 从 v4.4.7 版本之前升级的用户需要在 **app/Config/App.php** 中添加以下内容才能使用此功能:: + + public string $permittedURIChars = 'a-z 0-9~%.:_\-'; + +为了帮助尽量减少可能将恶意数据传递到你的应用程序的可能性,CodeIgniter 对 URI 字符串(路由路径)允许的字符相当严格。URI 只能包含以下内容: + +- 字母数字文本(仅限拉丁字符) +- 波浪号:``~`` +- 百分号:``%`` +- 句点:``.`` +- 冒号:``:`` +- 下划线:``_`` +- 减号:``-`` +- 空格:`` `` + +.. note:: + 该检查由 ``Router`` 执行。Router 获取由 ``SiteURI`` 类保存的 URL 编码值,对其进行解码,然后检查它是否包含不允许的字符串。 + +添加允许的字符 +---------------- + +可以通过 ``Config\App::$permittedURIChars`` 更改允许的字符。 + +如果你想在 URI 路径中使用 Unicode,请对其进行修改以允许使用这些字符。例如,如果你想使用孟加拉语字符,你需要在 **app/Config/App.php** 中设置以下值:: + + public string $permittedURIChars = 'a-z 0-9~%.:_\-\x{0980}-\x{09ff}'; + +可以在维基百科的 `Unicode block`_ 中找到完整的 Unicode 范围列表。 + +.. _Unicode block: https://en.wikipedia.org/wiki/Unicode_block + .. _urls-remove-index-php: 删除 index.php 文件 diff --git a/source/helpers/array_helper.rst b/source/helpers/array_helper.rst index 27fe5064..bed6280a 100644 --- a/source/helpers/array_helper.rst +++ b/source/helpers/array_helper.rst @@ -14,6 +14,7 @@ 使用以下代码加载此辅助函数: .. literalinclude:: array_helper/001.php + :lines: 2- 可用函数 =================== @@ -27,21 +28,25 @@ :returns: 在数组中找到的值,如果没有找到则为 null :rtype: mixed - 该方法允许你使用点表示法在数组中搜索特定键,并允许使用通配符 '*'。给定以下数组: + 该方法允许你使用点表示法在数组中搜索特定键,并允许使用通配符 ``*``。给定以下数组: .. literalinclude:: array_helper/002.php + :lines: 2- - 我们可以使用搜索字符串“foo.buzz.fizz”定位 'fizz' 的值。类似地,可以使用“foo.bar.baz”找到 baz 的值: + 我们可以使用搜索字符串 ``foo.buzz.fizz`` 定位 ``fizz`` 的值。类似地,可以使用 ``foo.bar.baz`` 找到 ``baz`` 的值: .. literalinclude:: array_helper/003.php + :lines: 2- - 你可以使用星号作为通配符来替换任何段。找到时,它将搜索所有子节点直到找到它。如果你不知道值,或如果你的值具有数值索引,这很方便: + 你可以使用星号(``*``)作为通配符来替换任何段。找到时,它将搜索所有子节点直到找到它。如果你不知道值,或如果你的值具有数值索引,这很方便: .. literalinclude:: array_helper/004.php + :lines: 2- - 如果数组键包含点,则可以用反斜杠转义键: + 如果数组键包含点(``.``),则可以用反斜杠(``\``)转义键: .. literalinclude:: array_helper/005.php + :lines: 2- .. note:: 在 v4.2.0 之前,由于一个 bug, ``dot_array_search('foo.bar.baz', ['foo' => ['bar' => 23]])`` 返回的是 ``23``。v4.2.0 及更高版本返回 ``null``。 @@ -64,16 +69,19 @@ 此方法以分层方式根据一个或多个键的值对多维数组的元素进行排序。例如,从某个模型的 ``find()`` 函数返回以下数组: .. literalinclude:: array_helper/006.php + :lines: 2- 现在按两个键对该数组进行排序。请注意,该方法支持使用点表示法访问更深层数组级别中的值,但不支持通配符: .. literalinclude:: array_helper/007.php + :lines: 2- - 现在 ``$players`` 数组已根据每个球员 'team' 子数组中的 'order' 值排序。如果对几个球员此值相等,则这些球员将根据其 'position' 进行排序。结果数组为: + 现在 ``$players`` 数组已根据每个球员 ``team`` 子数组中的 ``order`` 值排序。如果对几个球员此值相等,则这些球员将根据其 ``position`` 进行排序。结果数组为: .. literalinclude:: array_helper/008.php + :lines: 2- - 同样,该方法也可以处理对象数组。在上面的示例中,每个 'player' 都可能由一个数组表示,而 'teams' 是对象。该方法将检测每个嵌套级别中的元素类型并相应处理。 + 同样,该方法也可以处理对象数组。在上面的示例中,每个 ``player`` 都可能由一个数组表示,而 ``teams`` 是对象。该方法将检测每个嵌套级别中的元素类型并相应处理。 .. php:function:: array_flatten_with_dots(iterable $array[, string $id = '']): array @@ -85,14 +93,17 @@ 此函数使用点作为键的分隔符,将多维数组展平为单个键值对数组。 .. literalinclude:: array_helper/009.php + :lines: 2- 检查后, ``$flattened`` 等于: .. literalinclude:: array_helper/010.php + :lines: 2- 用户可以自己使用 ``$id`` 参数,但不需要这样做。该函数在内部使用此参数来跟踪展平后的键。如果用户将提供初始 ``$id``,它将添加到所有键前面。 .. literalinclude:: array_helper/011.php + :lines: 2- .. php:function:: array_group_by(array $array, array $indexes[, bool $includeEmpty = false]): array @@ -107,11 +118,14 @@ 以下示例显示了一些数据(例如从 API 加载的数据)和嵌套数组。 .. literalinclude:: array_helper/012.php + :lines: 2- - 我们首先想要按 "gender" 分组,然后按 "hr.department" 分组(最大深度为 2)。首先排除空值的结果如下: + 我们首先想要按 ``gender`` 分组,然后按 ``hr.department`` 分组(最大深度为 2)。首先排除空值的结果如下: .. literalinclude:: array_helper/013.php + :lines: 2- 这里是相同的代码,但这次我们想要包括空值: .. literalinclude:: array_helper/014.php + :lines: 2- diff --git a/source/helpers/cookie_helper.rst b/source/helpers/cookie_helper.rst index 1d5fab05..310d012e 100755 --- a/source/helpers/cookie_helper.rst +++ b/source/helpers/cookie_helper.rst @@ -20,7 +20,7 @@ Cookie 辅助函数文件包含了帮助处理 cookie 的函数。 以下函数可用: -.. php:function:: set_cookie($name[, $value = ''[, $expire = ''[, $domain = ''[, $path = '/'[, $prefix = ''[, $secure = false[, $httpOnly = false[, $sameSite = '']]]]]]]]) +.. php:function:: set_cookie($name[, $value = ''[, $expire = 0[, $domain = ''[, $path = '/'[, $prefix = ''[, $secure = false[, $httpOnly = false[, $sameSite = '']]]]]]]]) :param array|Cookie|string $name: Cookie 名称 *或* 此函数可用的所有参数的关联数组 *或* ``CodeIgniter\Cookie\Cookie`` 的实例 :param string $value: Cookie 值 diff --git a/source/helpers/url_helper.rst b/source/helpers/url_helper.rst index 722386b3..a027d2ec 100755 --- a/source/helpers/url_helper.rst +++ b/source/helpers/url_helper.rst @@ -20,10 +20,10 @@ URL 辅助函数文件包含帮助使用 URL 的函数。 .. php:function:: site_url([$uri = ''[, $protocol = null[, $altConfig = null]]]) - :param array|string $uri: URI字符串或URI段数组 - :param string $protocol: 协议,例如'http'或'https' + :param array|string $uri: URI 字符串或 URI 段数组 + :param string $protocol: 协议,例如 'http' 或 'https'。如果设置为空字符串 '',则返回一个 protocol-relative 链接。 :param \\Config\\App $altConfig: 要使用的备用配置 - :returns: 站点URL + :returns: 站点 URL :rtype: string .. note:: 从 v4.3.0 开始,如果你设置了 ``Config\App::$allowedHostnames``, @@ -48,9 +48,9 @@ URL 辅助函数文件包含帮助使用 URL 的函数。 .. php:function:: base_url([$uri = ''[, $protocol = null]]) - :param array|string $uri: URI字符串或URI段数组 - :param string $protocol: 协议,例如'http'或'https' - :returns: 基础URL + :param array|string $uri: URI 字符串或 URI 段数组 + :param string $protocol: 协议,例如 'http' 或 'https'。如果设置为空字符串 '',则返回一个 protocol-relative 链接。 + :returns: Base URL :rtype: string .. note:: 从 v4.3.0 开始,如果你设置了 ``Config\App::$allowedHostnames``, @@ -72,6 +72,8 @@ URL 辅助函数文件包含帮助使用 URL 的函数。 上面的示例将返回类似内容: **http://example.com/blog/post/123** + 如果你传递一个空字符串 ``''`` 作为第二个参数,它会返回 protocol-relative 链接: + 这很有用,因为与 :php:func:`site_url()` 不同,你可以为文件(如图像或样式表)提供字符串。例如: .. literalinclude:: url_helper/005.php @@ -317,7 +319,7 @@ URL 辅助函数文件包含帮助使用 URL 的函数。 :returns: 绝对 URL :rtype: string - .. note:: 此函数要求在 **app/Config/routes.php** 中为控制器/方法定义路由。 + .. note:: 此函数要求在 **app/Config/Routes.php** 中为控制器/方法定义路由。 在你的应用程序中构建指向控制器方法的绝对 URL。示例: diff --git a/source/helpers/url_helper/004.php b/source/helpers/url_helper/004.php index e820085b..8bc5192b 100644 --- a/source/helpers/url_helper/004.php +++ b/source/helpers/url_helper/004.php @@ -1,3 +1,4 @@ 0wkdSbs-$|+9paY+* zcNS|RAwAHsl#o!8mXM%QaYFqTCIb}vMauYYX6l)Q z3bS?Yq|}0Mf9~1JP6cKipFntOD&GY*Swc_?5}gb`Up+tLi&K~n4(ah|!+Fv9hNpiizUehXHdm}F@=1^aV&EMn)W zHss7i2CSZeFYIrltiWAyu1A{6V!Uc-HhxPNRvkY)RBKhcRoVoJ%&mP)9P58GPzrZT zN?yFK)f=?LZgX#&cacc9U3~2)bvyfWdOq#NfLyqHxYtv2B1(=d)Tf4OPo(s3lQ8}I zBpD5VL>fU@_?bK~q>Z8(XoRF6rx`Z3J-E)wblq9tEjBO2`F+;vr#`rzw|McOzIq#c zOHyek2Alm8cjgP8iLGel1Lc?Nh>uiQikt-&VcGX^8Jx+V?BSE~I@Dj|PMjIG(x)wJ z1Jc$o)si-omq%g(t}&1vgjyn@0#^@!9}?gPSd=dxksboSLBNk>Ch~vXMP188`LAmf z=eq~RRVAdQf#0gePNt@I&KCAAZ}kPjfLBdes%g1s$;$~C+uO1kn%EnevU%7#+%-WG z@(=(nZB1PasXT0L?3@KWgrEQOgaB}T_c8l(s(&7Fu@-)=C9gy!Vee#0#l!ZJ?d5Y3 zEGjB0Atw_v0TszN|85TaCH&mN#l=B@o!#Btoz0z-&ECnJor9mBpZ(=4_E)c1fhSm< zJ?&f!Jy`9WY5w(+|9X$4sk5<@rGty5y&cuvdku~3U0sBqKfmkfzyJMfoTeU@|I?G5 z^S`GBOpyKV4m$_iOZNYMH_%k*?o$CJOAk{Uu%x9eAReF(5k5Xnp?{wLKX?A8$N$w* z>wj8uyyE_krvK~K|G%lav#FDWy)DqEi^%^h&AB6*>irDq{`0&vy8;0 z&>BABpv|Zke`=zc2n@@7!tAED8#syoBswxn1@%S1EP{T(ad5qoHZ)dj-ZOo5cR*pr z-TXZD^6b|9xcOo^Jc~{?;pqcp{0~T|R7ejn#E|~uvI{4mQ36l6a`Sw_e^B0b6#4%= z@$Yt8e){?pPw#odKlcO3KwX1|dTMBYYmSO~93B>i^x$vzSEG^ki}-`()&DY1{0A&+ zn-BkTvy0=wdR#AuLtzjSD$_?Xaq)n$B2D(9__C^TZ7_QU3(jaAx7}`RRisA;dw@oM+kh@$A!>DGvCM;Kwbv0j5)6%Nm2 zYiY}k*VWZQi}BIg6yM5c&RsytH8+T>#JsOg8*_eFdwanI3O`cp>!;S)zZ&EobBT=` z7#bQ*U*BF1`WFMktlr8^nf@*vyW7j&QaP0UwsTDt+;Gs`uva;vP(2{9Gg-6XXHDsM z@vVW)5tBwxrf8>r$}FYsEdN?l-?IbgfH(2Doj|dYdt8g=OOc2B&p@=Z-HJhaq1=G3 zpARW_r*-ob$R{EdMG>RTSNjcGUTG5pf(K+X=cLKrFbG3>*Qba#hnSo;mVJSZW zgMv#&I1wu}!~bRqP%i8d;p{(+p3d(3QNm|mXQFNdZ!T8mxIND2+*wCnWt&Q#^Uix1 z3vOlxWet@hCJg-Na^;dhGf{S%UWJcfB~c=9MlZhVmS4DnlaYe^o9Ton8-wn?8ooUl z4T`l3epgfDI`(_$#T91#P)h&nA(!SlEP=!XR-NhU8M__$#b|-*UMQvCj~JS_W;vMk zWz|z=qzy;ENjcdL`Zxz&5UV?7EdzXt^}$|tsm=rZXWa}LlhM=?y2smNMTxZ(TRq+D zM{-vxwF54mr`I!f4SQ{1qZzNdg%hsK!D%5OWGCE8WNIl||&&be3L8mWh1EcQw9 z{Qf*wR^a{aW9X2xU80^_c~Mz&eWR3m;dRa>j%opkg7MupK}GeDm*xd_hXxoq#Rdwo zopw=WNO&h_HTmNkxyNFF+q~ySohahniQ)Cd(HY`=Vde{+tk07SYUEA|?_xdYe(rBy z+fzXHw4bFHuWuR-N~B}y$G=HDWS|drCUYHCcH8_GnvDJuBprV+>pWNmhwX&Mg8P%X zjjy1~b%$-(wnZh?PDV-X39L1IXGHNiJnTmN!u~qA(Q14zn zX@ng+W|~D+O=?>iBN%6y1nI%-WHo3OqPB*y>J6%C=z&Vl=DqD1pt(@cocn^W-mR|t zoa8ed)FS9)b!Gv`(jf=ygs zT{vj{$M0;)ytB$Rhx@t8kBfZ0f{Qm$lgc9EX9V_V!!k38KPGsWN0U;#EIFMpKwl-nI^VpI9Ja9ms-Nv zFz%ggG(<)#ZYKHtlLwXRN8#hI9L^tMI}_~VHaNL-=$0QCm;>HY`i_!&uSD<~Sxz!a ziy27@^jc!tz?Yb1i-4(3;T&@v?hIvzlKY+xDZGpQKzH;8`78Q-l;}-ukqrViQDZk( z&riL3b9LbE2yOm?HbN66WCUIQ5}z!Y<$ch^Yo^R#xDRY@XCim|CRhRv30J=4ifI8g z^})%YsG?Ts_Mi9LN#^N}MxmeriYguCh(QM8;;9OA!J=AFeDU(*^l}A>wiK4pvq@vT z^YgPA`^2r?9-Z7`uxY?koQ(|sW`fCI8lRWAwnIUIxA2{^%Ka74zS%(P7Cf#dHGZ;s zP~_^Xq!!qyCtlWfvtm%Bg4n=^LnA&|0Q@a15j7&7aP&Yy7^cRcbhb6pd2@ZDs2RV{ znwD3yczv-FrKv&Sq+MEEaqTQlxUoUlA~bZlE)bW?epzjr{5IhO9Q)zg9jirX`eInr}^m8Wy@5{Bsxka6E>bGQ$ z!OkUC6QyYt>BFrOY`XT%mu9ItHc#VOsJIpr2@{HGxfUsfJbo}6Ab(qiztzXtFjt{9*IWQxX=4`La>_C;nt%yD7ZqcbyAl2%u8(zZg4J$h-m=L zkTn7}UgLSPF;(lhVl(-}a9@|Le)GBY=#QASwGDpkECYYUSi0AC0jT3$yHW$Ku1C!4 z*P*%(=qS39W!AzjONF5n<`H9KGY*dMfW98+DvWJ3GUl0U%=o99*Sj~S*k15cg)4$Fx8Xg<3W)MIFcLcL zOfFXKN{6=9)mE6nmP%dWc3q))YP#|=^N{W!zp(5<-3A52`@-8IgQD9aKQv)teUBwH z_rV=q_S!{cQW2k8O9j@s7A&tM=tjDC@+=eV(Yte@(F&D~f=VN;8U?)ucyf{a{#L!6 z7thDCh9cTy_L1f%0~!fJA}!5|E;n9g8VOXgeFlD)9m0vQU7~Z#k=&`)0F*so%Q~>v zxUh>{uEsjbkDuaf`=g*g`lP4W^hw%H&xO_HW?N96gty#sB(Lt~1lZ6GPKXLl^@StL zo?{=_d4z5)F}wb5^9hXXbOf9~=XI99u<1AAzLvVM!6&Zj=)|fxuBo2Og8u9LJz4Sl zyvFnI@CcS}*L_N#L-Aq{O7(kc? zgA;o0b7OO%D}GPSgV6Du26f~;za97qn{)LCr^||b_rkrjPA@btfs_88=w!HaV|w0D zotM!ulRlUR9HHU_)JD?}!L(>~ZJvg2QEns21?wq4foQq%V>{xOpKu+U9qU$m5s!AlNSMOGy<~G}nlNI;IPK|^%56gS6Dv{us=`-Q&73KZ6 zl>?dyR#+s#gK50H)q5c?i66rmcp1?Ue{df{*syMD+qn^oxY&;b*v<{nI8>r77!tua z7=$L=YbBeyl81`AgkNynxHc0H3n_Q?+aU=>KP9xN)I5^~i`KIN4n z06$=t<;KO<=Ut}?3N4Hr^AU-h1rpto`0`E%vEHsEHiVf;6-5Zv5r#1@BjRJXX8X2M z4yVsW@iM@MehqN9mK>3-xuW(H*IAQO)=^HKSEWWSd~wL=!a_&3D29=ges)^FWTjrf zV{?`B6(>sA?qr_Hk`8i(kQxqY*(&|vp9MTKj_}uWf?o8Wg7z5>NZ1m6f&Yrsgu`j& z3gj?Z1k4?=L(leR8UiX&RdF_9sWH~|?V*z5C{|+?E4YFJ)B~daYLRfC2omnp#qYM@ zfkJZQ-=7^5s-S1F4tbd_0`$L=8x#Z6+CD$yoUO3FGft#GI8 zAUCYRX)x^Rb@x&n&o%f8(pKcP412TtIzo#r~z%R6Aev|I}L z;<9&&$TeQtDR7o?)jM18_jjMp@PR{in3EF~OHZ+GrjiN|F1T`hn+xrCnunYh76=4u zrlx#1;O3drCC&odUD;$oj-rvyEu=1rjPlUt!UJL91wr@y-2s}R7Ms~vxZlWS5nf2p zKI@z_JhE?9=$B_z$rWAfPS%1*y?MJLI}0roQjlJNwahT2fSjTKgfSwWKeYxKwV7p{~T`Lm=MB1cBav3zhLuwg910DfuD8ACyoj+l}TcJEB>A z_(nCMAv#pmo^YrO7ASpXg7_9t9#}kgilX&8ou#Es9K{Idiz#BMgAqKl717|w*c%}& zYUDoY`0ym*!L&wnD^-k~=sw+Z4|ka+oJ1*zE)L8LY$~nm0~^ycj&136D)TZb-ip2ePCiQ z$F2>%sSJ|$R`{h<$_!*Ch~`rTZHu~<@S+BTT6W_zfG!5u_iHOcLuS#h~NsAL#^M`)a*qW_Ut}V!I+E&axZpLHo*e7uGL}gs_&G ziJix&IaoD*dF^f-dolshUaK(6#RGx|5x;kGycfh-yltx$Q^z-pr zVX4cYguwz3knOe8wv@dxAy1LHV3IXplBHOn_q7^$9Zwy;tBbB@e!F`HBVuE&G)EU& zaZf|bE=_DubZF;&u%rw-Dp@_m}u4 zf91Rdb~tWyM_IcN8kpBxR;M49wB^jG-qLl-``H@l_Ac5`dyU*t#>-^*>rTm$0*>*c z52?Ss+fNu;^cS+u^2d2aD|hkp4PS{aXXB?F4~U^t>>nxtrk<^bTk? z7B#Qp)C2rJcquE)IN=RPS%V)jzAHW^@|~Wb8qo2vsa$$vclgAv2wj<^Z#&cxx`@UM z2?Ax0QZ#|tZIyJMw8{BAV-C~Mf(35dY1&t>4LrpWQRw8v1VKj?AV`qncoP^^QKQLo zg-To;CfYWcclZx6Q;nDn+jL~#c?@HHpU;ur&oyEeS4YW`1Ah!*1RpV4C~ZthXU4h4 zl}cq%J??1Gq!&nMRCr#>F6yNlWKbo29@NgRhXUz@zI^l$QzS`0!^O1Fz88&cBwFEp zycTWIuZNUAFSk7Di+aNkmb0DX)T3D#eKXWV^zh~s1xo$$6zhB#K+2=Ib1gZTAPA_0 zo>`U#)-M-WynWhujN-JWe4G5rHO%|LH(%il`fDy~`mPrWzPf`?{cY7F17|ya4U$5@ z_3SQwHxv$-LH?kEF$MP27y7zmWwgb?5~HSCC{CERH_t!{DHb|J{8$C)p#p3V_^qaj zb_!e6zOncguz32A(%L~{JgznZM2Qe_*k%3{m&RTJnLb|9RN>zp{>aAs$bTY|Ya72- z)*wohCaQ)Q#&(jSsj-KsgZxj2Tm9WH4=7;15FL=rsAd+mm!6ORh`R+}SC-!XfWZDi zgdAMKGNTM$s)YY=VWA11O?l>Igi{Q5wOG>Ns7&dB`57o0DTjaf8V}*X(=<2H`o<{S9Xyue z4lW3J*f0<6-H7kXq+*V>h(?3xzEXkc-LTf6H(|-_JbdAlqWmXi&EgPc>SF}S@8ZKY z%PeHWi?7u%2|bNh5Y!%vX|*f;zELqMq4$aLpHV+AtY5D~i-d2;eyv^By6mY_G#w>k zQ1vGNz`$oyWRpwM?tYO04?MsldDl~=#l`_*;mWMrhwtcmPM;ZeMN``$(NfD!u&XA3 zczq7-(vmwF$$WhH2JB^9|6ISx!Q7rGXeVE=r^B^;AII3lf=qzpP+GrT7%boYy)XC~ zO$^A;r0UyFTD~97F70Ql_nUKRzK@jyes+e6{t8+*A)?qG(U%+*&NC01Egikh^3@1z zSXW|_|I<2k-r^vF`BR?}qw{SI_op%8T~^<=PFH&?hhnaJ%IrOL?X?rw#ZQM<6GoU5 zOWwMdu+P{!doL8k1Le6SGtwZ#k|)9EoSqa+zLgP6ufEtO=CH@pDc1Tyh__WCsPhg2 zm=@cb%E}6Z6c8uq{;)F#*=f_NRE|UAg2pXug#V&;)BMyc1Wh0Zwx9>sSEn4Q(A8M# zetlYhu@PMJ{QUCTy>Q>x-(GU&Oa4LJ`eor6Jd=Mai~2e>;_N_eaKwVY`>euKar+z+ z%mzLzVBb;|A3PpT&9k?*ES1gLh%AjLO~cbhWg@$FFSs2oKRgkR-H4Vn50Qh1$aaa| z#oa|z0Klj5GyF{~HB!JSYNNNj3(|qJRhBKH8zg3rFnyfOdnW9Cb}Btf#U)fbmZ|m= zkJbn?2&Y75TZ*U7Z>zHjl?);)a#1r|IEejfUR_Pjd5s&H4cv`+m2!^y}V} zL19XB%#FE_O8mfCX9Bc30!uM!_ZbIr8B$iwzPR6b@VGakr*vN|kc!1i;gDg=RsxDo zmM_0Gf}RsXJBA4pmNSZfLZ7uU=a?1E+9bRPP#@vOrH@7?y zvB0Z{x z+pK^xmRVXMnO5R#$@7Ng)4n0!Qc?B~p)CCtIOLpzbw_?s6GASQ7h3)uIu9w zz4vf@>3VH$VAWG=T%cz-vFA%?jp87#Z%h6l?Ul%}We4|=`v*}r=J_y;n+DnQ&+U}p zZ+Rf(Y}?boFqXJq3mG?pWBv@1C#J>*zF_fPf}>d1*`7yJjSOQ7zV=(*@6OYjMPYr1 z3;hE6$tNuRuF|KoF&>oFpH&e#m^V|g+x*`JFz#?%4R_?rq((#iIcf(#{12_$Qer3% z`xmln5Ky*H=nx}61y%&(D5-zlj>V71ZXGdF@_ZMF!`21LiA;T?Nc3x`qd$u>dl(Nf z#X-KO651MGpz21cfQ;LB%wOS@NAOwS^EqQXCwMulCT|@NJo7<@WWI^`eq|5m(FR1p zI%0CYlzHjI_3i{kAuYW4Nt0~pung5uqb~@zkvnj@ zh&5M7HbYVA_gr*&-~Sux`}8v-ofKUV-LWpB{` zRIr_>V#4XQNGaTIp3K2^qE4X7@n&F2e~~fBZv6)QQSvtKO^`~*RY21qjWrMW6Fh`G zPD9-Jp+N5dRYu@G1CA5j=a8W3@6qoVvGbS@s@-D*5vL=vweB+c6Udl1b>GdSig|va zs8^|?xcJ|8Z);jME`B@Pn!UcXSil|?I6pjjLuBRUMvvv?w$(h2%y`59%{X99UcPTJUG|Xh1$YcFAWTPVF3n}^%eG7Fjq0CEn{k`F;h(7O*#Lr|7w%WWUVAI z4Ep8dTjd5gc_-^&?a(*J7ij&;hg9Zd>m*^FKd$iFrNvNXZSCpZsS!r|;mS&CIL{97 z+hJ#&_pdKN_4?^Zz zECy%iMj*_JE81f{ihkx5zkX#}z=PihDXOO&%2bosqxW~S0JugkF37Xhc`D;|_zQP3x@9QwRls;{Uj*$9C{ z2c*KQH_-xET(EM1YLC3sLOq*ShVfHrNPl=WlQ7WHV&DS76BPSaQEQ@zCEK1pA7 z_TQ!T$4n{Q-U&Lhw94}~^Re*xiEAsPXH{A(d!7mF_4({T7UR+m_bsHa-T>w;Jva#c z2o(zbG8W%$|JB435(;E~l!j0p3`#bc#Sr)(#F8*Akn(tIppr^|7S&O7asIsZ=N1^C z8*U-j6aceq;*_xz(k`!Utj(+%(9cw$q>n9WMdu`-ACi#mg#JL^@XL8kr-JBBAZqyi zh(xGsJip~*)wQB5L#}k|P#`YU3A3Ry_~FI;>o=yB(Nx5^tSnUGvM82g1+T7=z?QU1 zUCDBmr@yqazMyaHO;=T{F@qYGEYe?=c+ksMIZzyY$$`UY4D6dbBmyVW+~*56LE z2tG}1NCL^|)>}_Cmmw^hy_p*K@xVe==fO{T!AO)YBKpy)x|29;0tSht8I%~?v#up{ zXS7&DPTY?hr)h|#!EKgQ6)saMSI52Vb3%d5j~s`s%KEFV*HJY(WHX1D0`+TUJ5?s+ zs9DgN6@0}u@Nr9TC70VeE6Vp~9+wDeS4S2_G$V#qma5IPbAyp!QIdfl*bSBw(SL7i zHtYKLZ79-_<>+ZZ!5(c*`I*Lkb8eex%|e!2fJQL~c(HUs458i>)n z=zEae+k!Fo(E?fwd~Y!g2;vF%4FqzXvCpOJ9`5j$^2lBfGo@{%hYnenc|FPU&E(@j z+*)`h#l^Bk6=#iYFty1H7>I&Zx7bPAwTiPhGF@)qTVL_!U(2pBmEi#ihp>%2~RjM&&FUN&EE|0x&08_N7ywVfT;$ zoe7178Z5|#)YOZ#1NIwjvtzR3%hRo(9$bHOQve>^*dPG1!eXY+qlx>N)mHUHm{fmR zVBgteUHoX$gj>hWo!B)<@sM5P(VKI6K4p>&9{AQ=eU>TQ1-ia7UXtRibMTPnM(ctz zR)ABQfK~OXdzwfgrMsHtv@Ib7a@ zSFUSUXL+FK4MVsf3=rHx7=x}w>MhgB`{~`^@yx!w*~o)`fw}tMK&QuK(ZNsQiP%gn zZ=)#oDOvz?<2zjJUzqMP!MYQ#vM58N6f>dfy2GyLZvbW}t0x?WZOH9+tJ$*jdNL_i z>OtR+3YYR*avzvwaTQD}CmXK`Zw!x0Y2Ue^@erg0r}!tZGoA9&(bd_n1~pU}~9YERV_XLr&>z0Zv1j-lzTLo5YeaRfR0prW_36^YPDK z2}i|Gq4x|>1cWuF7dB+=3u zEHwQ^zf5XT&*iWJhFI(D0oB~blDhNKzw-Z6bigU>=BYvgsK9+0`(aSm#Fs_X|38`F z-&6rHDtFW{bJBi2-%-^2S_5juq==+k+T;EJpg{Zw9pT?^SWOn2KWMpCuJpM))}4^` z{Y&0IABuXcxjsCup1{O_`MQ0=?jvX^T`pX0O?j=Wrc`4^!=WsuItikH8?{EJw> zGkjo7On?01FY}02M=owoR!@C`{ufD{q6UUgXl+po)2rMsbulV$)Z+@xCv@U}n`aWf zSX^%~$ntMPkV5|C1P+!~zF$iCUBO62WoEA-Tz|X&{DFo+|8u#pe@GYqcN${!z-iq7 z|Lp(L)6)~o5}Eav5h4Y+pyIC$KVIhh+akyzDYNy8y}}R~I&CT_t+giM4?Xu|Y^@$dDiB3(VZ7A=!wl%BuY%RpL zZY^QFR8NqZF+bWYG^JA8CbUe#Q*m?Jx~%FW4rzSN)8qRl5x^v#`5qv_@QV%s?k`yD z^9jzoCmT_hgLhmET>e%3hSTAFjRrSc%6{`_7_<$zri?v6{yBtElgME;;QvZBQwG#k zrcpA@Xx#BoWM* ze_0XrFtL!@GI|=9Y0I=_Tm<6$d5VxHR6gldhgp9zp}U+QG+FSV(b;yfl~{(1Cd&pD zAdi^a&g`ph_mSGw8Wx;H-%GAiQ%6R-yu8$Se9|KoU>y?*q(enX$Y*X>M-=zqJEc|p z&g<>Vl^mACw4~ftaeDxv#qnr2E}|8fef1sKwqjB zaNUk$gl*+zFg<>YdaqUj=L2(q%vy6YuF71!E6~0Dk6yMNio{cZu>hHg+Lue=>96}a zkaUs)On;&JY zt6)22o%NSU6M(J>UMvQ&rtw_^>>xDH-+Y>8CG`#G^S1Jyu_1T72DXu0`Klb*L~ty< zJn{hP^lwso8z?TTBBSh8uPUYIJ?pQ9z<6Q-8*6K8Q(x}`9E38EtzM%?x%6RJDeYCu zEn*P;k;iXLOWUvglTliWJHEUtlFzYA8HSp zy<@SQn8cj7d~Yr{+?F8>lg3fP+F5c^T|G=Bw8&?>p7NdiUqcyZl$G{b(0JC_7!Z;Z~(%w`D6OB{gKE0rykej1O6k zRlF$+rXsfMTJG=AOEj`0FGx2R1(cg-Afh1W@sdD|drN`D(rlS)XraZ5EYQJx&JC;fM17;tAK1AlngCdVG$)0eHK zb?e)e$m)Cti$8<*+OXLly;biz0|*wo0CA4RbxvTxMSQK!J|z{*_vL+87Fldc%y&DFv!eO2U)MRvq&s=X7Z{FD90 z6I|8Htv2=+IfbvZLZW&H^GyU{=1z6H!#PE1&I-gcj-6>TaVK|-Lsdm#@N>WAcIg4E z;`RWl*Wm`0R%1OR+%mh7m)#7M4-;G^K&JiRRNYV63BLOH1A~h5n0WvNZ{kfxXGKEq zHeCRzW2KF0p?*`nEkL-6JOPL#&omEZLENN6RHr9p6B--h)?Jl%;?~eo?H|vw7UtKXYbAgWyIo{6v3dO0wWk_9&&AGOj zDAjw3H+wV=HI?NRP1B_zTpvg?2TinzBgYg&t7n~xv|Kg@(&YDQPlxTw`E0wIR65(1 zfG8CEpneY>hXi%p91qRZ?wMe2eOcr2F(|>OX5Oo4ujTdure;}$DK1Z?#cwoXUHnLy z2H!r8&PkE0-B_M9kWKwo?s(NsodVeyOsm%68^NH8>9~-?T!VWJK&9v(H}LmmNQJ9= z9Mta&1V1mH9xc&v=haMOzXBraYUa|~wu9dy+=+A6Wla<_idD)9XL(Ff#`hevYVycv zP$4XA)%a&YvrjTSvJ4>fGBzrIt{sM`9=r>GB^gBYnGZO0_tL%hA%a|fZ5Oo&6Tz#! znhe;!uk5U030C1t&*Sg49(%h@=ktz%sfw9y?`FM$2YllQl1v`tcHlj!Y_lE}P<*}s zD6pM;3XiMhbqn`|&3k}pSLV9ukSd=cGWRw`!^#(6`|WknM1Ag{n~whR_1&ag+<2!g zz=C=PWCB)4jPMK{xf=I+vHc7E&j6-dB*60-wGTymZpGDQby!&BC7B%*LOoq~G2dh# zDVWQTXohe11}B6s1)Ey>^NG&fGkLLA0xdd8eh59D@M)&bY%8zO^;Eg>+ZtwOdih04 zTyg;yjDz2{ln$){bdq&RoaVoCq$_E@ZRVydhom{wlW>Q1R63*e6?( zZ;!rEh{oQCiT~FA+u_f1D);#*1~S4LR%`)GhatlE20lj}KpVhXYVr%-H;F|`WVCow zm^!$w%6>s)zl{Y9o%90;d~pdrpgEq&_b7>qcBw!{B8-`4OySo@99gn~b}NTic^=38 zLb_MC1m(bKSi7-x~e+OZTdwC zOzSywLs4lxQ|Ydx;1C+eYI31tiOjhH z%;7Ke(k5e!`&+L)a78jS5VY!y)>o?W-5mFL%Y|$K{=T%D9OwYOFIdkPHipSMUKzLz zYzX5!#xp>5W!^ET$F6i)YPoIdR^VS-<&)*k*Z^!04+3E9RQ-eH;z6w*4X$jgAk4ut2PY@@I+AUu$G-%N+RpYH4k-HA&wp4d0KCD%cnc_z6yI?d zjm@7-ab~G#W@W=Kp8($YpL9>N4+Q>SXdzOH$z0ubEr{y(<-yhB>E=d9(|@+VPdy4J zzD?_2W~yWX3%-#6T%Rmu%yZLAA{^%p`iyh-(W_lfz>^)-(05`Td;LiJrz(-XYOb8LnwrKkEq>F{Z;UJ2uVuTJ z8|f9w*&;IhqUps|@Ux~~ENpD~a8bsK_&3=&!>Nv2f$J-(7;k2zd6@keIeM7rdwttY@G?gK=0%aPWP!%^^rUb3;(XA2R~U(ZAW5!*Dt z=YJw!-LroH)IRV44At7R7Wg)^+c?;q&h<)t8Gu&X*M=gW65QiEeh@1KtjgM2lE#JZ zn1r8Sw-n|2U`EDiWv*fd1q=Go>*19({+BkB4y1x-JGIYAcx+5M$(-eMQx~S3dP}a& zb^+Q+2N1U@zmD4(E6M}TpY;Jc-h>yG9FEJh4g2-h5*+Kpr3zWIfXz^EtJ1V9NYopj z+i$rIsC7OU1Rd%7Be+{guY};6?Gy9~A8_u|{%9%WOVFA|Xy%wgw)jbPx3C&EWAHCL zQ6GDT)IUFO0RPUxWxU!QcRecnqu_q+9N=*fN4PmE0(54`1wfL2MO3pADHyNSp}yg= zo@D-YfR022zL~u^xiw_5Gg)p7skI0uQi#sfnr>lSv8ZD{ljz#mNsM~25SjWhsv)mLr z=zNp6PH%#sR*5#ZJobaW6ipSf@LsinG+=ZC6o4l3aX3stO#>RXZhj);hECC^*cgQq zvsVL0>Ew)k%U<;_bt|?;#CcQ(@g6_`#|y6J{MBtUlqKc1U63bWUB2G0Q>$*upm5D^ zcf+3{?wm{Rb4cySyOl?z@M%1j%W<R5fh?b%9fgJ%mo=CS` zC6hO+RY6|)@)r;)7y<oV>cTAbpCrPGZvTnIZV(xaF;amB zS%CGeLf#`#Jgxa5TP#{#=x?RpEaD5--WV`%hhz${J3{Ioza7+cMgRc-3rWAoeQ}Uh z0jdV+xw`UjPcELwMRa!whn~fs%mCrzuI#NZ@s}6nofOtkiXvcd{Wi^)R}{H4d7UUn z=meB&B%wA8qt=;;+<1t}uFmTuJ25#=ulLu~1fUo&E-j}N_HWYw^)A_Ij=!H3d~v+j z{1rbz-Wn^4SF!~-QgkvU%GqxqwNS}BryZb%TUyUI);aHdPN3#^Lu6joa`UmIK*~+SSt4c00q&RslNK!UTz}3$F~|S5m@<4Za<+QkSP2qmxTk>EP2e~ zwU2&XkxZ*MY;3{?CFs7Pt#=sQHckibABsOo~(vL}Gpa;`5kauwkOX0CC zjGz>y{;FC02PmSlp7B_TD9IwKQ6i<6`}$rKajN`X|3a z4H`BPYSCut55RAFelb%Sd}aMgq{zB)_Dh~ZirGkzD5fJ&iNRaPY!6iVn65X~Lzf%x z*;ie{0b}}cy}~M1!S{uVK$61>xo9lasX#G4`#!f1I1=*7P?<~=>Utc3=3$GnLar^#&H8np4|XTvVpITFt_NkR(B8M3 zfRU>_?lNfh@hn^+12Pf!tO@W4RPI9v1@uwe^8L4|q9ersT(zF|Z@4P(25A65$3Gvl zLC#^hPZ5EEey0Jl7XMFdma50}8ROrORVs1-eU^XwY2`110z~=#+d~bnu2-sIf0?M5 z4D#iRC@1{Cs11IXzyry;VwxPWzs-OifcTT~-ri?T0ntXg-a-7X-=ARoMWaUV&}DAQ zza=WDcJADoKqx&uXN z=68oL;6l!QYxX^WRt#+00L0y>CICJgT6NpXnFC;2@3>E8J&*6uUBHw217O5dI^{-E z1Im(RhEQ~qWHzwhvMPbr_4G)!$L2}_f#Z|^6t5URrrI;*{c!f?A9HRqV}Pqr<@B6J z5EVqAbPGT5x0myI`LLYReBj4j(PHDDn^qLO=?LC=3vR1XjA_^ch*fF&E}oF|QHOW{ zf!QqQ5>Wc_Da{EeBegAGOQ;46lGe*5GZqm|(jn`w#q)9_biIl`9gnq+Hf*kTelpv1 zq*@YAU|9VpHpPS%z(msnpi!eHXcZ>i!!i%j-xT|yjsgaKrwmJw17eRgR;ccr0a#y* z;$~dj;Ji8AhAiKf%*=X%UEO7-3q`x8dNadtKFs7nA;f3c&bed=K1BbQNkkWy9`w3h zlPq9ugnsf^dc{UOBOHI7Y5$cH=l|Y)zyJaPgjVyNgh|@-ovIhAxszrowpN1slRe|Q z6#>@a%ZV&t8vs#!3#mo`V>$!i-r7@{6QEWPee0}ptrvR_z^p4U3H+Sa_Npf4>5GU% z6h(kKQNU-cK>$xpkywvn58M0$J~+a84ckz!)>G_lfFrCLSO=Yx_5Ko|j&}b-)4=yx z9|!lu7|k~tvNsVVYi@Kt-?5=`Z~*(W1q$h^_1vcDG@6g4UtVrzf zxH~9(9@fAX%b-{dA@IkowK2yb<-szqa9sJFq$}u(a8*c;0r_4my-N|gB%ZO%OWPH_ zy;SV;Vk!Obo?(m|Gf$mV6bC-k4k54wU>a=r+we)))lny9P&KhmQ(r3o2;e{LWwMtM zo(_>4OTD{zChS!iL4M`>Cp`fG!2n1_3jj*RMaf7{*Vt{cBY3bpcAqiR%)J2$P(k#T z<2DVM@&}VyQrN7s{&Q|rujA?Zk~ll({cjOsNPX@THfgTo5W?*G(S%Lmr7Mq>-|{vy zwlz4Vmz~u>T*6;e3BU&3b1i_fVa?jkbflw|zrLO7ip%~|bl`Kb^zbJ_f8l7}BvxMc z1!&pDUml1__n74oaG3F6CSVI@w?_+B;HjYC8GgWBC*|J$CDvDL35?8bkVNy{>H<)Q z8+3K5r@0T5(Aw>k)T9GNFnI&Q;CF5~*;}0Q#QmC&ub9mgpg9JOmc8bRRaOx^_h8hI)(eUmAB6W90PS{Ie&DKE`StG zA;&9O_NJ$CS4K*T4O9mu3Xd9&dzfbzh5N+Xv~F*1IdN9$+Gl~N@TZV!=p<}n0be%P zv%a+PSO!R>NXTH|mrMTqU7S?08>g!1)~3(b?7UI23sgTlDud!nYnQPEPya{&+gU$6 zt@q}N68{swLEXvjWO=smNgpR$6Z#rxjmv)$0L|A>r3}uc6-p+yf<&wAz4 ztd@~HViP5{+i)@<;@EB0_jPI7cSwpD4SD%Rx!15Bfx)MB;ggN;wLtjVS#_CYjy?&5 zK|2GAx+L6ICK92z;h5mNgb;x2jiqhOd)GvadG~V*?*Zo!M9)6{g$7U&{*}PLvMjor z`-Orfk!|*Jk!G14fN4fm+09K;c<*vBv@!y-mGm6LFeDP({uMh_lsM29;613*B2M{Eba;OVi zGU$j~F%5Os|CfA#*jNt`bw1F%1q$wWCE4o+dQ=8O0gR#p2%y(>t?1-nyPgpZEp#Mz zu~0^z*Lm|45M#A0o+N0c^1NF6x;qV&r1E(DK6N|)29LwzopXtuQCr{#AfUaEq^0hG zJFNliwH^x!da9TUOZd&9gzP&Fk>rX_$Q6ot*26^;Ri(3YLBu$|@b(1>ku(tky3(oYObi(g=`CQH3D@5w#c(%+UPt9OfHG%kAi zbZ&Z4i8W6ZMH4YSdq1;uMMA?33%yVUNTg|-lhu{`||LAAK68G0mBhp6%J5; zw0+-X_aeZ=^7u^l{(OJp>%oQ2Fkh{+wmF7}Qd~6?MWPS$DxC8HhzW2+X zEC&SMuK?6T-NiP$KrtV*+V-3SybRl)o5}H9i=Ws9piB3eIrcq@NBeKl+r*ACfynoU z7(;;aYkQe^5ca3>i{tI0GJzsNDJ$3CJEaW+nD2m7D?&DL zGXot+tPXVpV23@)X$#^^P+`}4yckTiE@TB@O)|PXdqA$BbYEzc2>cjMb+rANbHKr9 z?Qp5%lC5qP#1Kv9y<1+2FmkxLB>u{RHORN(5*wn)N6SRxBcwC4RSMaD#&ePJt6RyLs;R>;Y2+$hco6G1IZ|7?ATm{B3 z?dvBNJdA@^fVAWl`tE{rTV=bxUYT(u|8K(E<#6^o1c2$xYHBX45>>;!fiFSWn{%gw z>Mso_pZ{q2=q0j6(zC1@SU36ZoygYrNQLN;BNyr+ZXW0EHe*;@wuqPE|Ik4eG!pHr45AD=ewrvO_r%_iNm_uL;`oI z7wwP38Qvec%s<*N7feh|$EWw$AE9>A^kXIR8v?c5%ZC*}0q=ij7y-@&mFR?qQ-<$Z z0Sa?X-W8f=Cww_fTyS!RF%g0raH;|QRI2q#y41}XwH{D$759(gV5FiygR!0Ef3nO-hfg2g# z19hc60_-f>OeLioh;vbkZB-`4EY&Cq;Z*D&?KmOBy0JhYGJ#oEukXY^24k+yt^Wxt z2fpoK2u26PL4$GI?x>1uxM3*@4J|-QfUjb4wVR3(I?mtGxdk$|vqwM~`cLq=bo9=j zZ$ZvGYN9dRdO#LPFWKeRWdJ%GhF(nvTN^g4C?bP|NGM3zv_VLhh;+@+9nzhWf&vPHAf(q3`3W6Bi-+s^M23soTGT&KfZtb4QHLb_gZ_cz1DqS_jOHeeVw^K zTYX%Ew~=#eRW)$cr`$x_f^$cp`zKYl1t;<2frU87w}o+;jW+AGy1#oPjZ%#Zi2wF^00_hD{7(l@sTg;eiR1uUNOs7m{mR=xOzj>} zPH_^A-@fhf?Ar&D@mHmBd@Kgt1waI2_tuK?m61|Ql-Seo6*{NhZme8O(%?ttM?Y7{=J0iN9(p;Y1sqGo(dh;?jRR~y&AL(b_)7X6pTcL7|Zj;Ge-(9!-{<(#XS2@J+E7{l;#gOt00OY?gxjai*zqV!kGs}WJzNc%f z2MGB%ioc`P)+&91Hl6EVpE8AhJs^F>cOJ)(xBLDN9EXU!B=vlY?SkA_(Twau3o|y# zXwJdwMq9U{Br^#_yLC5MYn-+3hN9khAH!_&GjNIVnD-q*g0fJv-72fTf+~5E>n?9_ z9lBb|S-j&XO}2iHnFJJSZY>`>5n_Cd$0!KQIGx$Os$XBhRIIUSKFXUy29IM0fnYr?(WU%oU$i%9f8-van7!FZhCJV7p2rH$%&*SF|_hf9R3~s5r#2@ zZtzUsk~0;&t1f9K$c2l#N9(nF*|}FyqVd8q6|^nTv^g|7+g(rKR{jz|)*UH$3clb6 z5OVEeBGFB`6)U`6kESy&90M#`Z=9FAomVXEpnqXW1 zR?bN71HV}jMUp~SoPCRL2bz7&cEXPmxBg%#U7UD|iOG(UifhpHhiSynnDfx&VkR#& zmzI!W$d9i_p%I4lfe+uw7$N8U4#=4UXQv{9koA3I2pa-GL5{L zJ#)zK)FRyaf$e%!=f@>yi=tuX>+z0!LTddCR6nUH*eOUq+CC+2T*g%FCSaKuepj9j z)i>EV;^@V1YqRF+96l0M7o7YDD(8cKB`hKy=RaI+l$;Ku655-(V)DR6RqtqXsqkDJ zXV8=L>S>n6XqCD+1)Il;ayjn`Gy*khBTA2AC}lRZTZ)%Ad*odZ1+63UIU)9F0z6d0 zi{xiG082lbIsei&X0GH57e0H8HyafPd%V_m5!Lhuz1sud{mY#eQ7ok1DWYjaZ=&Uu z2MeyG$#W2);r_<>)m8TH{BudJ7G#lkxyp|x##ztZwg;jfTzUHz)PhSTC;JxqZy$@B zS__~UnZQ&ZUUwwzYbAvoU+a~=j;^%tPIAyPi7(bJqDWl}3VYHszaMq?o;Tp|`HG)6 ze~f9}>eH-|X+-B}Te9BUnY=)_&=kpFA&<;q+L5E<$W6_bBR6`Gu}DEq0|uf*t!a*1Vq zf32}oIXNx?^S6@;W`uX6*qtyr5rPN3gK66Mx7yAzmz!?|jpR}a+QpM!AP_dyDfPV* zdp_u)rQr*UY=A8s8x(w%ru^v}=}8`SvWFn&P-EBr8R_LQ@Nvg#qR{h|TcC`uRxguH z8uv@cKcHwI$X$id)`ynHB{}rA(%*-r_bX;%EPQzebDR)xueG}7{wHhpLp@UO3bwz0 zo+str9)4tN)~Xb$#vmd$+4-o>JaHnaWj1vJ(99si^LJ2tgB7eHK{$P2aP@28fOwf^NBGP>?Ir*BpWq$Q%cY{!K$+#Kx5DqngHC`maRL|BOU@9Rv3KW6l4 ze$=`d^Cs7~^&Q(AdLI?G$bzJwDj^fjrkd6t?cJd@vMwQjVlun$eZvP`QTDX0)A0m= zd3L@ZS>&#u5}s>xcpvxFvvx2rcds2jZ~5u6>XlV!i=Mm%&>(`DU=Mn=*HedOQyQU9 zd=nDzCf1fq8z~ZE_>dgeOUJ5Tbg}z4&Xf>DGhWsfwSX=WW-xY^tg;snMQqtTZYC{- z`&Y&kBl{*yV#B{g;RR6*J0&A`ou1~IFA%6KHs^GRa|+een$ysXai>&9oovPz-m|cs zZm<^Q@z!Fld`^06k~ML^kUZq*KFMZDP@59RYN;^Cb(*ru74sLxm^XYS51lz`M(Ryg z?Zt04BQMoQ{GF=d1A8Uxr9Pv|)S1-+TL;)Csco+z<$)R)S84NC zn5y>WXnDSKc!I0Lw;S+^s50wA_`sQE47Tp}yNb&|r~_Y1w<#d#l8S*RhuX_Yve|@d zg5#=_n|AgCaSx59-r{oKs&-m({Sf;@)=aaQ$vSq%g8;AHVh3PSeyLJbM}Qnz&VNkH z>~!|TXzj4bE&FwcZ5LB<`ExI#=8Ocl?{DXUk({p?_ZiT;>#DvBsb&7qjW$XYa_g{n z@mz1Lllw;I?I*+ICS8>v;J!aOl95d_R4T+p(%Ts_Vl(s!vw0>Nk z6}>1INXC&VLE`)mDe=&@hg$y#;?y!dO`;z$e>otS2;k^O^>1-5qbTbeyUVeqdH8i6 z8Zj7I9IXoHlk)p5qR(@yw(TjNm^axq(df?1G(VJ%GADK8xT=bZd07L(w0D@Z0iO%k zL4TR*jdS=p?m-h-gEUIo%>m@lZMGT}MOc@Iy-nr1Tb}(~Rmsk%yC$GGYkO=a-pK|E z1{7JLwJEqHKXFu%l)7%+U-29iS|UrcMN?HY&`j@R66M3^rN2Z_f2Qc_@^Ap+7H>4? z9}T9jc{S;E2PXQW`ksay=6%7zpKy%n^#a0zb*)KF6?-Q}6X|HKD{omQJhDnaMjiE= zSd)InjJU}=+u$s=lHIVE9KY;s6MUOl3gryov~xMZq^2AvNgvHSL{Op!^a#$k#=N7S zTQ1bSs9M$f%*j5q-bA^R2z)3&f++x8X$Jz{>$EKQQ*f!K?2$13V#sW_#EIq(5G>P6 zm3U7?aejVT<;w`j$zBGO!t+Oq%q9ce=DiYi!}eBjd{`zcdD&ctQ;=Wgvc@}62lCcd zxCMqS9hb-J$jO;rgD4m^wC}*OXu>4jF0)?;kkeHa(kkG-0r||pgIE?^p~JOvO@`sJ zF}ws1M#KO$D6#Mv-};wuEr;S~fnQwTIUi{A#sveTsMHv4 z2$111?~u@@Rv%9A+*a=(RW%Mme$hr6EUcCuZ4Y(s?|9d%|8Ra>{i>K?yXxZHB5Qlb za4Ox4sRu6XI?n1l{X7eM$;X*oyXDf#)^{-J9MjWGR$DU_JT5&octY39qA&EB#afy8 z^>`H+k9kh07$C{B^WX|Q-xe+FXl92diFJ#Of7FyLXnb-yUR8O__R-~`+F{aHuEwCL zsob%Cm2Yw81a2-RzR!`v^_SbcuNyMsRQo`qxu$ILB4IYDSXSystrWv9Kf%I)oMqI%w5C@A~6@*xm#Qifig-No++c3K-ZK;+mS z9Bym7Y&p#y0H6q$!aEUVihs)_VPK%#aei`JIbIrWNGcG;radzZp>vxPQH^5h;>bUa zRMnY|4TWC%2x!3Rl5@9TrbFU+<7wYKso}XS&_Nfi<_0it)ZV^P{w~;bczyB#V^&b|e&_gfj_&5l2p@^sPDrCJHOqwuu3hx* z`|}yvagQw~d}3`@XqxE5EmCl9k-=-EJh1e5!>wkdQjqNH5jJCi7t6&}mTTj6a#{e1 zE*zKyN#4C@=F7L2j z*KW+YBUo=v<5518>$0-EtvXh_wY8&bH9)zFDyuBtLiVi$^nY;CEvOv-q;*~WFl^2{ zK0GJSug$usp038(TPhW%LJ(uQq~aVrjX&v|HA;FRja|MA=jv82d#s<`i-p_le~)w0 zE9_mIYX7w1K28oTY1wac=Pl6c=9j1X7=TwYVb57s1%OGMi6tDs#pM(ln%N1$*~$zVBhBaQ3$AH$`mP?-4P)mT&nS|s4ZB6Vu9F4 zEtB7ygJD+|r0=z;gm&GOt_RV~4DyRt(2NW+6z0G{;(qJNB)=Rn{!#AUx6PPL85CC= zj(mz`0llgu&3ni<(k4Kcqp-CI{SYq?2#SPlUv3QM&W%>RRXb+6iyx*~IHy;4akJokQo9waqd`M{3Cv@rbo` zORL;V4?0SbMK=a^*MDfKpvI!^G=E?m62Ux%>E5&QWXc8Ni`R_Xz^nTPShDx5K9RbR z;*MB+n(E}xPT{S$Bh3{Ai959hi(;74qlLl=jzKmHp)SveIi0|$#QEy%`G@#v6XBAd z%5(OaZAiv|plTE9HqhN9>_BQ8OJ(-lcmUDg@t_Qw(N)itjWKZWQd6-*FP>P;#}u8d z6Hv-S*D&wik;!;5U(%A4{rb~M@Hbr-u{QREg0-<{CI9dj&R(1f748^OeI!sS&4&8Y z4olm#pO?OK&q#xIUW#;gBEU}Wohj|NMG26NRW*Q=ztAn{CAhIu_#F55J zU#H8quHmF&{cR%2&|Wp9d_17%*5J(GtrXGgzr17MYwF}ZG*r}|s9{b}iRz&OvX@67 zxLg9HKr3*sF-cdbWH_7RcpRA#mBIvo+rauUVA;W9$aySV!v@)^#o?W-#Z>&$432w= z2~PL!>Vx_PJOY6w(%~NnZa zVmNz#er*nFG_XG=Lzd-!$NObs{sW7L8iH(8o(pnLf8z7#R$t$Nm7o1X@9ZV%)#iL6 zY3=dj%K!8PVx0eh@c-*KHg-&33i3?t+hPa*aH-xKd&fUrJoOQz$tgEzW)?hjXkZW9Ph{khv`q*q3f|MXVz&O-ivufH<|g^BQ|$Z;70 zwiL<#(04dLSPlpe$}^|Gc;Osuy0XP&@9F8v3e)&``{t3WIA{L;)c@V8ne#7lp%zB+ zj~V*k&#So=OX+&a0rlonZjy8Zn@=sSkO4#pPMIGcCRxB6gl|sIPW=OX`|r2L1Fqps0admmOqt=-eM1;_Hxz8RQ1mq@ zd?S&=?1KQXy0$@rLX;^6aJ^)kRMXY~(1hAhul7J$Y=iO#=3@SrS_T+DPkt(VI+d5A zwZYn!6%5P_EZwi5Bc2Zf=rhl&y(7{pEoan=Ov3;{TrgH#iQ>1%(=+(^0>1KVJp0Rg zKrsNDRXqWB19I-t&=-JcGyvFW*C};@e1JuFik~(hd#F^oIk_z5H4p_!r^bVNA8lAw zV!o|jWy7Y2va^q_dmpZ+i|k`n_5rKSt{a4*3xLSa!7Y5Dp%N<#Z02Wb2yg4O`?wFD z)aP*5lwGVjH&|>z$#)ELe)~c8DiZLP*yK_gL5hfh(D806t7gf!#`&0K@JwgmRf5zd zw84wQdxQLWELy4&cCtt|oGpdWFg)nW{XWq((`7ZBW* z^&T91F&qEmyRo~(as@y^WIgRcKT7TbAcx>n;yACgIE-@0>hT1va-qs!v6MHi$3Sjjc^Sm~Ln3!TF_AJ}BHpNeS0|RP1dVfc=!&mY7HS}+Q)?8!3WL751ePOC7|yqzf!-j|QnnJ* zH(^=CX)DJCe6RtLA_1PFDQxZxNes&9%gd%XYL z%9!sAScuL?tg!N*wBgh9Ajia-bc`(JXuP0}?GgSsDVfuIX%BDk(y{^f$QdLEN0hpq zqGlmQa<6NgLrAFRi;vcmN`TaEhFodd9H*ttN` ziAmT^iwA@W4f7lqg{C8v<6cgSqhxxr9^%_UNUvh~nji0#0!H!2V~@~XZky4{emgqd zD0%g)c0lKJ9yGfftzG|;a;m=X1<)ZViU&M8F3)|(12Ch@CrcxE9+A@{C+oVR7Z`kA zUw+#EI{3|F+n4tOeU)}BN8W273(;_7s)nisvTv(@Uu|n_P_VCW{jq{w5F{{2z$lmG zA|Jl`t;5_^@|A`O+YfQ)-$?Fiv4-=-pDAXbPFWTFm3}*<=@Ge!w49Ek1N-(0yVyXMSVNsv)tdT$g32u|RegwN=S> z@Pt+@7q-XivAbqUx3^=?D=(XfhgXdiYnn@6P=)#Wa~}~hhqiTelz_nYi1%k(xs$Q3 zG5Tb!GK$X2!e`E&C;9I`+Y!R`#N^FpIqZJw@JvO{@cJ}UAL%JSCQB+F`_Et>8C4C6 z2ch(^*j)@&^&rZ@gidRoe|~=cflY?@9Cjgj&1Ert<-i*RE7b+^BZbzIdn}IE2`(f` zB8UB)j*I=X{JH&tr~>yEbx#HbfgFXxy$loz>RINtWlra8zMBPq1=Y^j1_|w-LS8hI ztM0xSCEH~rtD5>CZ8=Lqi7~OJ; zyDA!WayTn=u&A1eSnRSj*4FI0A@FL)&=ab#(Ij--jjh~*VBox^>`4H>!FlN?yZ2rY z-3=teCx|cS6IC$;47~@W9QGt|L4}`rdCwDH2Rv(R{hrsa&r@@lhSp%~+d#Mm<{vzc z&XI2r;H*i)F(fps25cZvG1RO0U?b@+>YL{d~3Mch$Xohn zP*)Xg#G*45E#khggA}fq!J?s^FW|%Pw(%UUH5{eRlq&W20$Qpzo3c+VP8mp;t@8$@ zYgYGAt7HZ=_RS1YpFYitNfU0sGLzD~GBY!qK^C6f;$Sgi3lu?7Wn_BN%{5Ztv~f15 z1lE=OV{@*%LG#|QqG?90e8T}*+`X&i0RNP!`+!47D5jOC=L3`##=m?gBddNiK|^<+ z@eF#{8Zv~Xc-B;`h!r_lJh+4_0>c() zv))QEe`J+mUZ|Jzl+dovsYPNUD`sX|*|i_YvaDH)d*-aB`pe*9kgJ(1h-wK)3x|;= zhPfumEbJnowrXDH#0V8zPI7ud7JQx$1`|(!Tca-!(^mGZ>>nuf?8N$?!MRBP@_+x- zCQv+klcN|>OV13CXW$)j4KQABU^hyFm+?6#{$8<%M@>QPBtD}OBifOTML@(JwW|j@r zQ)=jF7DQg;J^Ultc$QgMpwQt2v7lPerJ%zL`ZZ*7pzM!CbvfHbC|RrNIx(wn@e4Z_s2c?={i$RTRvXUwT_?I7d5MZw>Eak#4CL@x#Jqk^(!7ZQktB8_9DWR9$#<%b864vR4|j!wGA6q zxPNFq;&hZpi;3hVdpj~KbU{+#G59tRVvg$yLi>0-z`mJnU~1p{tpCD%zo{JF>bqPN z?2pbeGkxA=Fd#RS=?%XR@vo!>b+D>+od{>hZaq1E;?UXBvL0!+kSoelO#g9>9lo2E zq+|`h3c=yAf_I5qeSLTB_`xnYZ5Ja@_usB)W$>uu>ZXO^Q=GTo*J@BRCOdEMCf}rd zhE&=qjAB`#CscJ{L4sTf0)sxtaKBvC>|jwW=gD2vx(qDgmBcZS*XY%3l_y_jvX(%5 zbJb5rV)Q*Q{r0qd(LP(Evrx{+`tNJ2Sxii?(iz^H!zl3MG;8>5e>1}oERg5c+F&}xF zjdJ>_QQ7Cfc{u$d-3waomp!FXMcbES>Q3mUa_OOwF?I5`Rdt>SS#M|>^^22YZzZJ< zLhp`s#l`+{tb#35o&Yd}&waJ8)Vr6d!bUjb@LgyV&2j0`)rVBqr8iKxvIM;Mj4l!n z`F8p)lNH?Ke+&xY9Ch`NZ;a=cFZbT{6M;DTDoCFVi}4Tg$5W_Vhr2_IgW>%*V&N%J zLb4*g6*#??fCjiO0fym*wr3?dNRcx2Iw!}Qe*h_>Y<>1*rze{`4HE5E| z;YK=6B+gf&IdMNuu)5NWCN>KqzALjH(gt|r`TocGC z3K++gGS!p`{PE0n@=Qen{nsy`jl-J0A9X7z`Z0o?ePbl6RmLICY}c5hv)K^3qj5Hp z|K{or@D&cNA;PZ|^2>ibIgp98+{gD{^0!eQxi4)+d0xCuu+9SJ+?P&HL9tSRS}}I@ z)7ZTLK^SMli$Q6H zR=Cq{OZ>7B>9yzu55+zCFsn*LSw z{5q(OwxPWb)Vp@1FdSna>#LJ~O5W|R$1{gt46=-omUXBzi>8Pe&$7KlpD>@pnjd8|MBs@z>Y6Fw? zUB2PI(|a<~3oxHNSDmaNo15A_Tez5PDPe7g-MlgS zGna(q?;tl^eCBb>wE1ERQ zJnbt^mICWHEpYW?hJ@D#koHZwIa?^hS{RMG%=5xz(e5$NbQoldI&YTdE?fBB2t&Zk zw1ZJ! zezeN>>%z`Bfe7e9<0X#VSc=BbcqSV1(I=WR3*L$6n9v->m2dC+h~1Y|6iYe>cAo1V@iTp2J8rtnrJHS+2+ zB2G<+9NjuEx8B!#a4iT^Q++2gR2ZfJA66tu?o#rJyVKwe^k_uI%*Y2@fJF{-x+g|> z;sLCR)Q%SvaW^aJ>fEUkGSd~L&Z^(n;_m0_m)lqb2fb6Zlzj3BQEAx_?;nKR{!E}~ z1AbzmbfQ^iR*v|@haA~i6N>pE7wJ$QP#0KV zcik$9Ndr~G=`C< z9;=W+zJ9;anXKOl4@IXiKn%!36YN_Vixb2iMmZD`{<;a1zHtbTwFx@JW8&CiPEwcx zd2PkOw8HpG<(dj8TvgT3FsIVE_8Qh*qTCodvp5vbbX(RfH%TngIp0Ibm!Sy;LyIq( z?79QnF`aSul1kO3lS=sPPX)a!^L%>8Ky)G8@M(3f!@bY=6pe2VJzR7a%OFPN$KqVb zTIyjjm85mRU%s@QX!=0(VO z#T5%-3Y52%&SFK*s_8Dgws=6uUb8)*>>CWZGOIEWJnc@063YF>MJ9OzqxbQ4{VG+w z1I7`)(x69q9*Th~Qb-3;tbHla-Q|L4nO}kTa=s(z$O1ru0q&scS)D1be76h0eOH|A zr}l~KphmrvaMkq{W;?7JN5RkUJW;-_lMViR#(lLQI22=kFp{;R#_3sdw|a46R)KtV zeA$7LOHJXmGebG zHtSUdN8=6i*?*m~i@mb+9fZ`wPJ3U%2{60$n< z2|Uv=hXOM=zrHIhut4=u*yG$H@rcMTC(&PjCbbaU_b6@|Ghie6<*Ud3nUsvLz=-Ku zxp;Ki#il=jCNpNr<^koK|2Z`O{ngcVJ~P6?)aCW@-~FBcG;_T0*$oT~yyG3FMStp` z2qo#QV=V}?#5QqH>DU>cbhg?xbh=jnO7Pb?0k6*I!d=jM!9{wG+nHq-3Te~>;DY&Z-ejZ#hom*8tUuK z4O{%xKK;LwA%cJFOJm~^J&haKc$R@idI45dR$YC7_&c6l;3>Dvvmv;3=Z*$YcV7ZE zfPuBZhaA1{6*_?crI%}}Kt1d&=difb#&O0c=6naw?DY!3bG8_?eD9&NN+6Ia8X|1{ zewdHQ_q0dh;YO%4R>0p=tZI11-E|})R8M30nwa_S{%YI0rLc;~?dx%}qdN7@RM}@D z?FqO0x*nX;?32z!LPtr7f#wD;ik|vGia@|8M{h$Ae_FF?E6!_s*}WBet6%LS>^dV< z0`WttDXV1V(^_TKL5M2m=g9Qn-#3^4^nvR0nFdwrf4O!7@o&$SLS1;FIY)NNGzlwH z=*gkdJzP*+r@fC0jKXPJuPRv`A*X#$j09g)p-_(L#UH13n#9x=XFZ3rOs<>ZlzX2& zA5$PKs$BgzXF1@?+~|V=m0b41$?pry4~B{@Oh7(#At;=FVjCZ9KLe^G_>F1>K22Wp-?&)yQ+zLobmpcO%&=oR`g2wVfgXnz7FsqUlqcvZ;+Mk??jaEcO zL>RTSw18Aq49xwy;FJGM>HnrV1)pPo0s*|u4Tf(i;z5@4-_z1o$_Pr3T(L3GOJ0d%rW&JQ?tNkeLAn!m-jb6zha z;>uJdh~>Or*%hPcL%yoQK>t+wkf zU?tXgf*Rg}Q~Ut8#1;Y12U;)9(1~rpVj9X;FO-29HW1M~2W5EEf`J2~%3KY|w_f)a zQX(5WEMJYR{pi?kDUB5+;`OP21dI8rAbY5^6vkLvk#oXG7V4(Q2wSpmb(ROa&(A0FEo$;dc({EjcW^r%l^=%KwB9^msx47EVxQyj|w5LLnkGwpZ`017e9JS>& zV8#w1N%$|$qFljg@sGs6s=fo zeyC`E(6lrpYR}tAx6Jcn4n!^(@B}7A(oLLy1m$@#6VhO6u!H zHA^f9*tB^UvkRAiV_H;M$xrtMaPf_|qiYTuDOl7<76Lkoy^h=}u*L0xb`Cafht)=% ze4kdG6&V#Z2!bEdCx;*`TZoOql%U+JTw4r)++Z|a)L?~+4OSw;3Q+A;6cqI8LeU@_ zePC5JS{JL><}}w;$AO8f=fU*?S`#kCM_p^{h}`l)dzIwh~3gSF_BAAnly{mhqIP2(rd@xY#C!I`4c>6dsCax+wf=UOAr;f>mU( ziB4y6k3?}HLT=SGe(<)shSVd>-9mB;DNoL!uZKUo);B-b`-w*nm)1s=b*+eF-luAh z?;J-2!?thU<{8Dazmj2H!k3^?-jfeaQ-ujZ+f`v$IRRqdJF*EzewvWwF8zQNs9l+U z!-Wb^M%#lk&fl4-R{0}XCM*2z`?>N%Km^;fIn6wGcv!2Zx#7o&j&Z40*Fb>8GJwa3@EOwTLc18z1C$D7j!+j zy^jx@#SSpuZd3Br`%6I8V<(q9hS3yDWIkL3pi&bNNQp#P+{;*aj2i;Crxx<6%-vP) z!jxEP3d!qsWi@-5u9TPUKBjw84S0}(^p{G|slI)>g4JK5vYCjyV`qOm^Sj>QPQvEV z{joR+K;CK)#L7MWpg`i#p!{=ZhRwE50GG}Z4lyiutJ&PTT}$|96t&wmWYxTXe$I#* z>dhBvy6j@5(vLnd@UdPm4=q0Tll3DH`V5Sf#_j&YY4Myut5oXh!B(Av2H}Gdw7}uP zzSi6#he1X_YX=P@gx zBHgc>v8LY@Fvp~7QmeT{~lMFwq-|2o(9TRW!f?BQe z2?fWm$A65kuxK{SIC$ECGh2~Bz_jC`A1AsPt%NUG~L^0HM&42L07O*f7YUY zn~@EGd6(vTBcuprMh1al3awG$cBaaN z(lF&T?XHK99(kg**LW-IXOTM);bFaMVEAdkPq1y!TC^VTcmy z-qr>6#x}&HPBs!x`mtbi1-wt`Oa_FfcxlN}NH-2%3r)j{hxs%0?{Cfz2TtJc}4C3iT6}XbtcoIM^#k!df9uVBghch<;s2|Nbk`Hl*9Pe|Z5%K?qs`y-BCzxs4Mhy}_#p%Bn(dO`V%BIUG?@E<_<#-OHF)b*JvdL6o6TB1^r1AOu4mW-U-=q%#B|kcJ*@c*`=Im0YKsVg+LIx9#4_!y zO25F~*MW(Owd=nbfZM*cMP@zQ^@=WnQy~u`_-e*7br{Q9Iy=WwJ=ul?o3ylvsA$Xm zR&x!X^5wv{0Zi_Ra1(lL4QuM1g>BvJFXei#F`f*6$E_FA6Y9Hnvo=}#{h!&3nHs}LB;xoiQ? z6R+Yb35ue2J#oVqPx05V z$lFDdR?Y}M5ux96W?KHq6}8ko4P?Xz1}R(8i>>(i%C*u-oLDNL`CH4)`$kP!*# zvx@>2Q}Q1^e89R5!G}z9f~Oz^wbk@pM*g|mz|~-4V%qj||H^see;xksFAjU3JNaF& zn5LkNrGU2g)Qj(rcbl7yz={i-v7vwg2i)8t%9U%FQ$fmRT&klr^vXyDaE^yq9z5s{ za44q(h+9-xSa-7x;mEJyJFIVQ3dg~R6j5!Mr!_VuGQArSLgY(@lp~PI~F`)7xcm zf~%F-4w*_V2MmBB{YCGf-;F@*>*IL*7ub3M!+0t#`4T`hwiY)z_Hor>e;!SoinPQe zoq4Nur7}y4^ppt{ZlC~=X+yQp=C7e6?56>ad;p5#k3!k-fBRBEt3DP`)gPmC3pi!9 zcAdw)T0MB)hAH|~Ks68Bnzu?^bsC&%>WhO9X0;HzTwTqaYCb0)eP{7Hs2#PLA?rO7 zUP7tj?(dlDqZXws4ZZes1UeH0T>)(~+qf;N1aRSofcQj$phx_B3}D$wgH8A#SEIO< z%VzXCuL{6hMLQ|kwEC0wX%?=^|N4oVKEc5c76?^7IoMgPQO;9SRMZDL`N$qCEC{ty zS%lz}4=jzCa^+hvd9>gdH!_k15;UD>ZB~ke_;>L%QTAy-nH*^w82XKF6dtQxQ-qGxj`#0s|{Ifc_^6 zgOd>vK~@1$BSEz@>*V8wgdAPn(&I&Xtn{s)5myzxu`E!l`9)IYSN|I#R3=Iv{^Yilqmi^Z*c_djqmrjhP zLB=S5Q-Z4*tiNLG1bS9Z>#&3T9X!WehLz_*M+Z+ye6dY@e^SK}z^(hi@nlrxzQcv9 z?2cdzZEI~Uz=mM4Cw9*Zm%E|LLc~Icwtzr6dQ;mAL@NJ+m%e}ht{sv=zXFPj3{QKG z{+B}epULnk$G?>c|0c_#T3SWjr2~X}OTf%(Y-P$K&R3Xf;kB*AS-zky9mLbMD(FnN zg0Ei{qU!$2TOwPqS@c{bFAXF}8x~{nI`vPITq|$5N_3qG+J+Qn-gCcrK21{lYCmJ3 zIeEVC5%T~U2qM9o?0-5F3YqioZ4*KzUFz}gs)4m<1=z)0UWcyCjEwnVHzoF8+Bb(X zlz8mf3q7&_^5*h35O^B|%S=Y>Zc{Mz?9KroO9Gucvj_1?7>{0``4W5CEP<14bInpS zM6lqkhJp%?Qt#@*uaH2dZ0{X1@8HfV)F54<6QcEgj#Jwc)fZ|8)8s>y;ewX233|~9 zz1_UU(nYF5>NZzu5RO6?j_K3cfGo_`%x7-OeBi8;e6-5^a5<>X2x8UkYd9j#f1 z%!8s?N`Ox`wwnIZ6$GdIfTUq8r$rP1j=EJxomNLjRGa4o=RJ#U-D#wEvH+^dWzuo| z0LW4talvOaE1Z$WMn?U>Ctf{R12skVJ4vrxIU38^mXwnExr*x(Q;i2tTKt|C1WIxB z0_MhZAX$~2bt4bH8X_c2;|(;SE<4wr#rt?nbXVph3Dv%aw$Oqrlk=~eLXzagTO5AE zaxN(Vyb_GiEPNhJoMB$hhg6CkAlTV_M3^5}limFA5$RHI>R|8HR)HCR8KU`k^C0^` zE9E*B>Ot;y9S@<-PK)iPMK_H=>h1lO-uAveT(9LJsJ{{~Mr@_9(K<1U0D`m#Fp{k3;&_@(>1Em8q~2p)9xF zK_!(v(L=-Me2HcSsAY5bfhEwsF%jom0y46P9OZjKj==RnF4u>yYkL1ct`rnu;4v0s z)m1f#($Q>NKJb;q8FwrwcEk`Ou&vhfeh^5H*`QFU>d95Gm26@@cjW=9F`z-$qu|wi z$7h^&oxWZ(=~tAXnLgJ5s($ppnYalgGi^37DXk5rM+%Rh{kl%{bSa*hLMU^S5fEx9 zZhGJiTnfm2xCP}&=Bq^Rl}f&vQu3UK%u(sM^kmIPXxLYQlRhfl+uKW4o`*T>5EHG7 z8HQCrvWFleP%L1n>rRUw_lzKOaZRwUHo^~|>=EG+Y7z|o1Kx{3r&klo!llv@$&42 zSsN}aAML(oI;SUtdJ7v5V+Cuvyj+}!`L;tH=~~Tn{j8!-tbJIu6fIB6OKY~4A}kl0 zwq_j*2Bb^+(cWrP%(GAVs}s3)5@E)x+?f;6Gfv7qlNag^J!j(_LWtRca*K-Yyfc>3 zVznvF5Vz6-a%jY0Hg39TJIELZWh~R=6!N7k`rW<`TpTe2HP*JX%O3&0vKMvy0Z60Z z^0POG5S#!)#(p)>5RUK5QobuRtg>+jz6#lg#;2#d(~TZ)HYa+d#nP#>;7r~L*xTI`$4BX>L@suU~4@NmCoK5Y1DRM58t3e>CSrca0B~P|2scF zF7Fp%Roup@G_y=iPl;$u!=KL#z3R+;W48V(fY$9E8QDB4hAD#s3iFAJ@n?iDm+Bfn zOS^wdjYrub4V&(8?0|d6JZ6Zcl2I+pu&A#OMgrejjxt!aA+FoxeUw@NS@U$!o~21N ztK#L}VH3~UmqnHp)kjUMTnGntiwooi`BIj^5*)7~)|b2ZFHkQofn5i-gn(2yKl{nB zFci-1?_aRsP`t3jT$*`YIwPf=lpdHv2i|YBy~0Ec{9-CH1TIcO=F8Okt+TVVbj<57 zZnn3sWBOVvXO)Ut_8t+sNI}%r=8$?NdL_s|`+}@~EYlRPb7pp}a+(ea1so#RVB%B* zg;5?~S)${|CK;3%wUC}X%-lz;RHRC^4ssDL!g?mjv1hpH&bVjQWh7lP;fr5Q+FC)T zt~piR;7S{6F3YvZC-931>o>zsOQ|y9qu-S#k33Ann%-_}Fc-k5D+2EqJA|}vzOTzw z71Xe>@hI=`vq3J`$Svr-Kdj84aT#HuqZ$DrEXu*@GKPZXrOF4NwqvC$20BlQZpvH= zx!jiOdlmOl`O|iWbl3wlbUuyZys#VXw+HF7gYvRLm+r|xG{B4rV&Yzy=gMi>ygy=Y z62j!_g_NEfUem_4H#ec}>Cmx5+r`MsBP$;!7Pg|MoiC|Eufm-NSsk1qi@t_Aqt!Jf z0}KC$w6Bhfs_WXmK~N+FZwVzWLg|)95mAv)7`l;W=#C*2P$^OA7U|BRQ$k`GM7q1X z83w*RzVZ0zJog{(_m4ky=FHh=pS9Osd#!6-*Zi`Q?9-eQ-39-uEIRS>K6(3gh{=X? z#(d=bcyFkGjCqu~V%XK!Ce-6uQ;GK(qqDdmeQB4GL;S`To zHVREbuY{(2NlFs4CVbN-pY_>V4CL-vLkG)m9I`mXO_Gr4=b7WY-^{9>q#xKFyBI>V zVz?=ornrsaZa0Nbdbr)vka$3|jrUs^?&GPP#It1jl`3EFY129WiI@bq#Q@?0)qOR^ zzEn9I3LWzU-M%JyyTGfaJ-)Bbe_1PXJ)>uTklg!Hbt=tiOh|oyhrzjwI{%2k@Ucr6>=-PFAiIXRy#EB~%o=}}p!Cg-yGIkzvvLv#X*pD$hN|&Gkd+1DhP=}yLIH`RGP~yiu^cu# zJwiKILQJ7`hfh=-$2R-?g~$C2dDqzBpI5Z4=qr=V3g2d}zJg}C|5#4d;7gH0T@u$) z@=lwI3ZS)nlbDa8KIUbgOxlRdBJ|n{)E!*xN%1WGJRhZ`#6Jq!xlY_Q9!uG0XWBil zjR}-bo|tY9gmy-8_t$xO)uHcSpqmXvftH1%yCV1=mCV%p!XUG!jazOykd!6NKAU8? z>_WJkWzpcAl(rG_-~s#J$(w>jW(iWBOhgdN_NPtISIwM!vz0&yKCEhs`nNZTgV%^h zjgGU^z`Mc9+Htm6shphhJ}GVgsHAIumbGGE3I5*u?);B&=j?nKuSx5uiQ7h|W)Zt{ z4N(LD*%VV+ZAmF8INcae^R;uIp3|zD4+?=j58&r8?Rch8qFa&l&4@xg>DX&#AjNe@A-M5^2d{{H3u8YL=ury}ee4wnjE-`1In(#vjqc_`&9aVr7G> zG^hLd9nWrzr6P8h2JFx#Zq@Uh^81UNZpTxVSL#Mgdq+j>6sk+w^F(t~=n8dZt*_<0 z&|Ic`#bQS!^cv327~bbrsVm&yJ*qhDY(q=bEZoN=|f!zw%VcfKeoT@`lUw>;C@$ucut0qi_W z3txJX29V;+rQ$^Ns)yC_8cR

RmpA=SZ$R9cZW(LZ`r5WA5|H_j9~ddREAl4pi18q5q?nBF}>0Ecne=Rg=Y8^pLqUB%4b5=8UqD zO6*jVu#TdiDuf}ch;&0XryFuHEjb5r)UKGUUecYt(`S;E(;MeKHPIJ?P}Q!3^ib0G zL*UR&b?rKlhSRW6!aH-9VRB(3o4?zuPs?>KaL}n4?_#>#zUIg%|G!C|U*J+8Q}siJ zg-QX^A->}|{Qz4wiu#RYitb{v&%N&?Sdwfwcm|XBDpF$C8Yt#Osx`)K%? zu|aJllU{f$hFIfb$#?vPyT?}lfOL--vGhNu&3{{vdh$ZG=dj-LN3rAsvBdm_ctq-l z{yLd=drJ`pvfy|zeA0M+Kn81e23dmlH^@yH2YceH?A4icC-j3vy%)QYk&%G&`e@Yp zk9^K`gI3ax0;6o%)s+hT*LK{Qg_p66x_+I2@9onHjNY1^T@grBsq5<}r+)eJ)$%(l z>kwrXVMX9ZG{cU#CALJFH@NyX-~L@8NGref_-F*1x&hGp9{-$_blvzP#G)g|-uOl5 zNbwCU_x;%iQ&)B2F!{iGs3;Yptc*GEI06{LFx*-LY6&Ox|rdZ=bi+X>+8jD zljTcha|@Da28V{iK^aC?POh}G^BL&rQw(mx#c0XfLwyL@C4%OIFKU zvJ^KMcDM`#gV6ZqgIOAn9{K10Jdb*5kL43mjd$tlSblqJ+Xrni!D>dU&wXCEhqVfDS8X zOUk28_e%S~!(9>FFS?Mwbd5FyI{C)Vy0Kz!}*xiN;*To+aD^fcFb ztP2*E_p_96+|11Kv6l=10?C%Om5IOd82)YX{Y(U9NZ_bme{@~dYcvj9((x~xViz*3 zOII=ph9A=j)^)7qI#xCxz=e+4iPq5uoPp^bYM-fS3KrwZ(7GE;;CF!W4Eey+;_J$8g2S8 zR!v}R4O&9b4mi#A@7%fLD2~ZQ?hh`vRBi-p4759t{s4fjf;_qpE2Hq^;X-g=7I4{| zJQk)~_`TAU^c=IC`&22Sc2|e&sa64B`VeR|cXq8StPblbiWb}I(An2L=z;?XjPvsT zTbYtVS=f(pjWSbXUP{Vg5Jjbf;_C2@GSq>5aV%oRQWwz07@~CB=Fa_=b zb1G8N(Xj&9D5G4`F@S!+K{>C#9C~;j$PedW;tngI#<>Z14f$h zM-Jx0Kr3xTVj@E&v0o5C8}xr-{Yy8t9I7BKw_Iq_r*E<8V=3O0u~bUwAx3<|2#Nl1|N>Hp0i&j;fA_uGPg@P`DRvJJx&S zM1+S!RutED&yFJ*nFUYY)rz@v-1`G>$q%aQig^Hs7{RGaUx@@<6q?e~e!$oJjiZti zgRXwC%KaWQB&@3K)E)+9Gl52<>jdpRje3umOW@WEl!L3ynjV4*Vp+L!P_|6-6qP8x(`MT?J@OXSx+tiE)Wp6PEzliYYmlK?;%YEJ27%t3TS2VP=4G~7xmgapYqK78URb_Bg-I0@25Rqp zS$&nJpGOt<-ptDd<~NrYn448#JyDBES*o>$(0Qh2=6M3p0f2GNzA82DiaA;kmN7HS zlz=3Pd$O^!cL`XHn%;U8Q~(_8AP{4pZ`uZlR}|FF-X7Zi>CR~dEc2bBJm^V}&-}o_ z&fmpUEz%Idt~n16>jQiqXj{ki&!2AsIFW~`SHdsh)z6R%Ifzf~$wCD?NB)_GOs&$v z>{Zq$8t$(ow;ZR!huce2oxV=0^e4-G9-GQaK-{a$1^IAF!nqc@o?*%ywo86?2WL4| zP}}h5&Fc#Xs?KoxjVP1sQSL1BEV3WS?iA8za7Ll#s@HZ36EV=5sxRew1wSHYBL}8-o{GQ6 zPD96{poaO0bJ-Q2_h#0ftgMAkY!DPNiE6m!G3#g;ae9>_c)tI=Dye#{zh?;Tc@m=t ziSj?rdXY2g7eqA1pJ?kqBVq}>>KS6gVpRqbLg-rwAh*1>ADU8elX%=_+Q3Nk-gK|lx3@pV~!6_={578(pu3aAE&G9d2MR{~xVG&kQf z6{$iKM4TCwmW_{G6j#GWR@QwIrPDbgp{ao3rK6h%p3!`~6Y|azshf&coypgn-V@Ec zIX!)Q=MyHP`5y@03Pd+n(Ao-c6%>K)9YF_##OYS(=@I7a#NfFbwzn}c0R#!|ji?lM zMX>A6XD=x~7j$0NV&J$7#7|sU(vfz6rr}gZo)DD>==Om?Z|q5-b=8^x!zcg%v<3Kp z!tHCjWRd#cfXzN>pb860(uXb>tQ><~hJFA^q1f>TWWQa;VvR~-otALPd^z3U?DSA@ zhfV1D)ZlfRsd-IQ_XUvvv8h%?=^vim>~NAJO{i(&kIm4Nn>d8C^iDXT{&g+i`DnLg z1QBH-r`g$#ma#7dMJwAj%DC%B1Kj!PW>{6f?RCgdq5N);bCo>}gyW1Up=*1NZQHww z*Y;&AEB->@KMz$^@u#S)`2HjGf>!gu4UR9zGo@lq!fWcSFEu`B;!Rb%&yT$0XGE}! ztydMru2>tGR+*xa{K|dg@9x+T;%2n1fyFbs@J{LR0T4tyERdVHk8hX7fq_FbtXJ>I z*JaO(DO042D>X-E)yo5oA$8fqbBwOy*I|;L>=U9nG}@a zsQ2u{?OAzT(DZ#~s+^1uh+O_j&5>QY`@QlsEt{WJ2in6mB#x{Mi8VI{AKFKmUH17< zM3~LdX6K6FSc6fKWwAR#T+Q!%>&mHwEgrJ&Jk>7$q_tLY(olA`Jg;)Sk4zP$#d@if z{h!FJCepicYDi>NMXUwnZQoCh_fJ@sA-bROnN7`RcBfln!bk$*pJ|qn2J=n3%g2tJ z?26$_nxB9HbwuA#If%EWHol&meKSRQa z*c$OqUSy6Vvs_m7Ao1O16k>!e+Pu?znNr!`)wQv(?WkFYrdNeS9TU;$4KG1RGmse_ zNjsVY8H~@!`AT`eH$56pzJ+W!Lr>4YLX%sbm+K3lrl#d1p-TH>Hh$oZe$s#59!ot0 zIe%qa@P~f(^bZfyrt=lakpl-MZ1BVS2{>n7K?{4F&1<4jdD`)oQwLAZz)%*!kviwB zc)_w7^l)x%=Jk!HeCIqnC6~Td65Ag)iNbOnJMhk;vvPlkC)R#b)z)^FkBFb5e2K&* zU=AJSe;g<(56K^>^kep_Rgjp8rHhm(T29z(U6j@0!&Ra9K0r-yCZQx zoEm1j3@~8S!zK;B_-TJV-N`V+c>`vu8zGLeln9NZe2R>-)EESII&UYkQN43g%S)-k z!DWlEjZQX{7VoL{(ik;Fe5bn~QFd*iRw-ZN9`e{ZpB9&Ns21_R$Z^u5eqoCeSOAA+{F<+UxJ4s)i7O<8WZ+;f_-sH?pu-kdh+N!}A@jE+*2hni1w)cLolJFsgy4P_PJ|O2fd=^Swc{HHuKH|r$lJ|_-LuEE3 zY)H7r3mllk8)E$}#wX{UdrvF|;`_eAX6E0bIN=5+IqVq%L}iIpF9Xf;%63|fhgskS zUx_v#m%7>SjFrhsyz+cHifoTL<~DMw_2kX3w{pA>DvZlkQmAzTyE1ZB6=kv2#;#bz z#wuwx#pAPu(mt#4$_S@}xv?Bo75l1#p?3Y^`~12#r<2do8JSC{FU>?H!&#JOir%?4 zk2mlsbQ-vKg`FMqfPT8Fc*%EBQwE>}f2P*jvk-GjjsnO?yQb=rJ+MbCxd7I%=uCt4 zy#ZZ8{1-qHEL!5x#(0fq>BMb8-BEmm+LT4feqiFuXxu>wX}0spYo*IjGMz^wf+u2K zIO)kLs#>;wV>V4BwRhJi&Q6_8ui!fp&XbRt(fGYB)U_FPy0g3BqhF>qxBu}s)5)eNA&x*kNErNhqLX1N+sM4L`V$T z*>m0ev~r6yl$FVMGC?(DtsA~uhOlzkkzmd#-Ac^g$<8#tA6P5{FTIf$^Wy$S?^$G3 z2RFr`6~`zypF}R04(Iss^dNqQr5C@L_qJC0oAw|IMOw40CbFJ=-n-$F z&w;{2t!K6_;|j&6WEEs*?O22b2f{=qI2*{coVxD*HBU#~N_{O>EMJhEp|S~d zSIJu+I^^_Y{R6Cm$zUYe5`fOXd+S$fupt6XAK-3LP*BJ%+Le=I86#-(t(AP>A=+%W z=Njwwk{^CLrgU;=BDy+RT%oJt_qY19E%=S(gL6x*R*tuXgoXeJ#Te571V9IvXm@U5 zVWXe>paqc$p*9MK-9qwB)0+B z%>dKz0{RaG?(OU3J(XYE)$KB~@l27zme|^3p4={Pk?`OLKs?np=ulBJK)=#hww0Sh zGwX`qUd(aP^wPCOV8Md%iTbo8wh8s_t@LYxE}UG@!FOs4+Ga=;hWA)Zw2fFI;~uks zwn{Q=ZI#PUlAZw3cP8s#sJ(r$nS2FkLmMeEQxXGAfHCt=wgjwPXsY%*<;!k(WmAwB zPD3s2EV1PE_~d*Zfw6ONAddIfR+{dyZz6Nlc`=-RuPQa;N0^7YySs`KtmG^16cbS^ zPb?B+vlBGY*X+g|Zg@}-ac`VmAQflX-pW9%e;(8A+iK?K=C3MpKt^V*+`?qI*gCI} zJHS{?9CuWn7j_BEM{XeW z2dVzd!bv8D5#0vNnre#L{c8C}Q;mM7)Bv`{CLuAwd5k2>XA~Dl?C~NVU6O-uL_!Nk z9ZB+I@^4i3F#l=bNtvL9(U0Dj=Ch40fR2y|08p52VtGI10{MuT#2Ub>j7*FUP9>p4 zu|Y>yr7-zcbHhqid}#`&EwLRbeJ$x*x-V0>hk8}9-0a51+opScEwzJE;KEeS*S3W$ zg;*Mekq$qB1ab%cHb&UYSq^f&HdE*702W)h?ky=ras87;hOMYvlUp5gwIhL<0MW96-odj_G-1Gz2+=iJK*t^~k` zmIm2*U$C*)9LaC=%}E!(cIhqL?X8wJ(7x0l&P{-a=u-OlKr=n{>qyUqG|gC*i^&In z;8W{K*^#$T!-qlY8DOd6n=AA%g#=1Y9ZOJ`kSKwsh@5TS$j#Ay^topLTjlwY1CX@` z%B=QK0P@A41jW90)2nx__0YJfKQiZyh3B-5AA8hDSUuJF5G-u(4b2`jwiZQ=S;bpy z>TBm}o|T~>`3rn9FP~SE=8{|(>KWc zVXvF7fTcgJ041Mckj`fZL6D4PUsiZ(dU{n>6yDh>DHG~k!eL3u$nY+{Uw^vAyI^6K zu^9fq;;K`VObT*{8c3&A?S2XA&(!{0bNtOM7KF z-);@rmxb71sAZkUU0Z>JMl(_8xnbH9!8BO_XzA8QCgC<{kr`hitDFD{v{E3AGz_|4 zEvLV}#iVwBb~gj*x71WMTfx92O-x}mw5NmLM!yJMsq(@z}h?n+BssdR+R>dVZNoN;VW4PWlOEabyiVf!&wzo z^K4YgFJesw7 zHM#0KNAbM~y0JZ{)rhIk;?j~)V>$JySBf>s7FGj=;X5qwoA8Z;iQMfGNFS+Z6r5+m zy4V()>_rTNl*`v0>LgTrmcbP11we~~G25tXtez(Q?{QHEsm~*Z_RJ5NpBou*Mq(SO zN&yzQqdWiP!ZB!+hz_uoDJe@#RJxYXsinMlj==Bb*4Es9qZrx~GB_+}v_H~OIXv=F zbC}n9c9^w}fowZ^bMgAw$=gVkA$KK#L&Y_8zQD1fynX(imnfKeA?aL}ubA^t9rL-xCvqabUv*v$YpQ=!i9MQox+3=FRjK($gI;EKL3};J zxe(FJ#M1Tm%jYtZWt`#SQo|!M`JGz>FUImOOV|zcad)+AaivZbtrcm22fQDRC@#$z z9jkL$LDtmCxmna2Rl!P@J}r+2?lhH&I9M1x;rPp9h-qlt%T^`+IC*E6_LJFG*EIXB zVn0SE!JSqe9`Fp%CWYmh)Be#3%_9Mk(`xiuhcnf&H*~R$zCTsLLdhr@<)+qA)+LoZ z+NOlk-1+fvq^RPDMwyk>#kXA#l1B?>inLiG*QMIt*-`yf_1<>fS%`yx9sWF>)Mntr zwAcxM<%%-PK$>R^FqflckwW7mW6cU1DdqJ@2{j8$;yFFYQi_EN@<#ACD_NGke*f$Z zv{OnAq`IJDqc)gU2oz`q3#1&EZ&EwhbLx9V#BKt8TeBbFlQ13he2opDw_)f7BLPoX zGDC}V4o%*1fLBg|l2rl}uKT@AlMpLiO!=aF5 zqAq+4V0QZNvWqL*QdwRR%>$r%kgYbB(MOQ{4;KKYR6{zaj80oFd-9R#sJ#N@!_G

K#e^y#_=AlkO9dPwt>@aq`*RKF za4+e0WeHb9=2To*N42K!w!O%aIdeLN;@d>WYaSz?`Go1@+w|E_z>2<}r(H$3OUhUz zd@ya6hIHFzm~-RX$PEf}%nPT`O}T11*AULCu#dmg^(;VMD=TVJSX^^n zTL{UPv^rks@Z=IY+|29uluSK9rc|m9g-%xMJ{#o56LR5nQNAu}(L=d10zIJGAPD*P zM-#W><;SL-K5-mseKuLJ?+-}LV^s6NW}ZA1dfMf=8OcWK{55h&< z??tvHOJNJz&nrmiMF&ZxRLU_zc+!@oI~}0Rf)XU3Ut%_;ZqM1oaBQbFOY-GFx(9MK z7XnJs(jk=Z%+;)Opfkhl{RlrBsBr3#hM@9`ZvK;^uO#`(r7;>RP8J=@eZ#83OOF0T zrFM}jH7tJqIayuf;wOD|HgzSw8K>f^>_dec^N!LHQ@(Y3*@T|_k9-LFh>THi^R?n9NyCDT6*Lu1_ zYy%A}ZKXU-V1+ymH9|y!F}Sr?I7I6O<|u{*8Q2RSz=KJTM#GF@>PP6!3uC9O9VD+H z(mGrof+xkx$Cqoh9lLjk+SyZNj9hfboIHN-O{*U#&p1)t8yA%lqjd*eS@(TkwiMpvbuKU0ik_FhW`ns? zqQr6Y`6Bmlg)eE~C@!J7uoIgckr0qw{3b8D^-uy&Jv+y0TuVqs1A?+B&mIOe0*1Zm z3Z&4qyNZzK^lppEDq32WX9r$qCh#$*Skgs1yHT!h?BVy5alcYnICauc2}^!rc4ZaR zoJgkA{wu;%X>?PLq}y^LH8oeKOsM4g*B=TIF^yG8cV1`ZnYCrbWVNbPRm}R9m6mM7 z%qRwv%-)zB?Y}FYe4Yrkm6G9y$7vLBNkdV|o`FQH3){V-R#S2})H6|*+nOR-NWL#B za#9|VRm=4_Rvf~R5)zTTNYBH>A(ev6!EPDB4!>X&$%;X|TiQsE330*JnROOJRu(Gd zM!tEx_M2-ZufG_JlN0N}9dS&NSt~LHa&n<#B8(%SX(>lDzPKb1rACk{#1~=88AqdJVXk_7D&h z4}pew%R2OaadEf!NPrt47VXutr(UM*%d$kcwR=MXWPsGmn;K?h#H4< zWa5wH^rjRjYCjdiw<9$0t=KBzS@^&x3wzvq-hJJS;|+3}M76R{Db&Vigh?>Sdb^Vj zVya}kFe11^2pS?P4=r4#YD)L#Iz2F80TJtx{9yjunb-y!lb0QiYm7rC-!aW7cNcyRwfQbmp;I{A0{#f&<-6 z0v+tM+%{?vbOL4YiIP!vkEE$s-Rk0c|IihSTk`8G69a^48j92PDbB?AowT?0lLQpc z_`I}1S2gaXF3r`9>YWRYb%dkU)*>2QrjD&5O5?hwGKurJ-A$E;41OU(BUDO{yPp+4 zQ)SP%kz>A`ay*_U6+<%~OGz!C-L3pQqpmMJuz#pts(3W3G@R%|C(0?Dl-DS8svzFY zcre+p$g@1N&$#sQYYj*ACXMorEbGMyM|D9=y9nr3N{eAX`W=MiQv=$ZPknN{dIvOV zcwV;AOGM7oVLCPwW#l1tB{BHlM{zGfx>D0NKaUz&H591Ou@AfRC#Iglky%xxbd?_6 z6l2g?oZQi;#WZWBZ&#e%A@Y#=Qp5>HH6ZS`^wH+l@Lb3(2HGQnzm<7dKHPukn>2g- zsp7*Ax9|Jn-n{-MgW##Mq~yhSn-?gESqKQ;;oQHy5GOPl(rF8~K3g8Ab~j<(UGI_~ zJn9QDG!{94H#vx*l@BbZ`Yu|36%4=h$R}y2%39%Vw{b_PV5`s! z>w|7RqZ%Fez1$ayVMn3y?(G;AlH~o%AANZ1 zU-RCPcXJ~NT!C;wd(XpV`^ZB@FO_>W?bllpvLM%R*OvzKsEi@=uL{WMy%+lp8eZ!{_FSj9ui&+qX~=c3xDzNqy71@sfmwH zO#UBF@?V!Gx#HfMU(4R<i+IX#e&!=K+xn$0+#GDd4p(0*+MG zK_GUE+Oytcz?&7{FAVkbJQxyO1p+C#IjCJ*Y0wC0b&w29kO6A1DlW6{UnJjKVF(1dEVG3RR%PfMl@C!8r}Pnk!$^hQ ze+PnXimv#Cxb`$vEF{o~ANPb_(i<_3g*F0?jV7N9bXNf-?ikmAmc5_4aWtc4VKe19 zf2iXHp7{iqlea3I{yVDJez8zerh0J-Oa$QsdfmARQ#tjvNcN*7Bo*6LbOFVq87@(H za1m%1BfVPIN7r`fKG&zJQF9fci{%9;HRIN(n{C~EHnHkjaZXO<01&7a0D77L(5NR; z4N8VvsieJvb1k8sKmp@5f4|x-%Zj-O{c+a?=m~M$XDlyFQidRC`Auq0;rlC0&j0`I@$|YUiqy$+wW8qv^xrZ@n0`#P<;J#kN`PWLcX}ovUf%X>N{S(h zzf*8v(WX%$k#OJy9scyykleq|z<*htk3W&avzBo9|`cdyqFnTZ|w(b90?`ugHU8 z^Ms0eF8IPXm8{AMm}pm$hmy+9_{TrSvw6bJ{OznUFp8UQL~?F zGk+!~;wzAyjqQCQ5x8^|VKb?hGH2_0+0UF$-KlH8FA&#=4Y}MHMGb0(yNzIfDf*YM zO7HYJbw}K4D+3I4k$<&>GB6w-fNnPbi&vPG*ABkRa)!5L?#`__r~5lI9rzhh#tLbj zZK$%67zkpwHjbYjrD+*}F4Z09EcUVm{_YNQpYY}N<4$oeN}4_6*~}ubcE)}&BeK+5 z;NHdxLl91d2m|bvmSgYhN3OzjSiikY==d^6lWw)pnF&d>(V5{ zcT1O#zw7tsYsQR&?|D6y7~ka)b*$HVdv~dhkz#!$(;^Tk)ehg`biWFaqETRhHUZ92 z3MRQ2^2{RTx&lBY9KdbZ^7@)_)D4U_*c9mOBE{@gAMeD+V;xH~o-hb`I@pSzT@|#_ zBCZBRmz5<>cak1oUk9H5lISh#27D^Ey@Njj*PpYp#)Nt67prRk(sC-nef`>`pi%p$ zt3k0``VU-ox&)gr@)+FFUguO=E@7KS?HgQKSNcCNl_`K!$Cz}yW50USV4>kT9O3_P zzq`oG(RR|CE@`yg5$=FFvk9a_lwOhtVA-#!|Fx-#}@04v3_3iI0=3w;m*KK8k1~n8&rJ(!Jvnunp!;eh`kTrvQ}fPMI7sI z69;-n+ch;VKg8=}HTg>Xy03=mdXHPL{3KK#eK8#Po zY6^tPR9rVz|6$l&ssVm|Z?U<47Des)`RqRED%S1%|8utf@wQ3u{QP`3UDPiw>KB(M z*@W-r?&d}<{+u}j+k5oJ?Zfxzk#eNLemxf(ZtbG}Kg@T`>E{4YHd584#{L7y2R_El zc`@T2j+v_K30^Q(xhY6d)nCcYo7>$!)O0+X~B`Xpta|KHkP>n9$7#ada;TZ zTe!7ojKe!u>AF(2j?G=LU*3$iW7l_1!}j4A8Z5#X!FW{Rh`Sb{E+j8E&QR zI-rSV%#uDTz{izREF+Y#e43OET-_j-$N%jMKxn}aZC^i>YsR3MtcFmEtE4Ekg z|8rAGYh7Jk*7+5zwM#!lOVX_ zO8;;vu;bF$aSE(<9FDPL#tjc_BdSyZC)m8P-Mn&aPAVJEBTXxNHa;KQhXaJbL;dHQ zN^XEdyC%Q&0@m0=QWdCO8KtrgjD}<1O{6i-=5lp0R%fGw6Pxd}QRdu*?U6KY>+SSW zHi&3qTLJ47z>E*9+y}6&9uRQ--*1W>85kJ&td8}zIQW%HN$~XabdiD4(*N|lwTsEp zn+O$brs0k;jei*RKVAwD^Y;eztNY7 z<0{~Zt%Z$soFPlJzSw$Ejh*l8Swr4CSc?8Il2e%=&MAdjl^xJz^7D8nx+=tZcE{QFANZK0Yk>I zxzk)uCyDhSyGakgGGwpfW!VZ3&)jBf&0?Ca`sYiR;S@WH5T83GV_p)-zLYNK7;C(L zr;;wF;u+#!aYGU>eoVq08GqU#TX6Y9lXt<$RHl3Fbu#p>w+n*XdFR`1-Ynko+;E>D zpfEi#zg#{$F%v40;E52vZYbZ|5*w*dI4+>)0+*`LusCyyg&Yi|Rm${iFJN0GyG~ra3XvgGQ^0HRjAxFlO3c$83)e;=c<;vzA4}wIakLY2WQnUBDCIL^Kdh`s=#(lxV#?Jk2X3 ziAovKKO9XrX}2m~b{kfPn;9o+V5WcS*XdXNY325jlW`YKM9&r?5@hIN7 zBotf#bScmLTgl_ULOV%MpR~Hglhh=z2{~S~ z&c6hJd~ZAKff|W_-PsHSw9t~{-{!=jwmxHg93dOw4q#wSK<;1;cFfz!B^70nSJ4Pg zr!*?sj(sq6+CgXsM->~$p9-+m9!^s*?|~*=|GLw?e0^~KCuIvrEvqLH93=}8Kc<-i z1axd9<)Aw24|p#bJ3$UF)e3fp$)-n|%AetMGXQ-)0a`ZH2vo}xg&3{?IkE&8T6iuL zqu@Sj*TJseD+uV2#m-m)nE1$v@B;YT%Ze^z(~6k)Z-Gp&d#f>Tt1Em zIp(PaiMrcBuutM5YhL+_zNyvZU#C4B{}E(;bX;4>X@a9__3sEBx234&%?+&^?gzmrA0!^Fg$5Dd+iIP`imExUaVu>UCmxIJNGzcsJ+-faGo}dr5F}&^=ayWiaO9SdQ@dZ1G zDu=Ja4kHh&%7*5*{GY;VE@tfS?tb-Nej&?F*R6VVI<}1lg&!gS8C6x&_nW(r!&BFU zH_H);MW5){$$Vfybnv-_HY80r+wL<+<}Aq-W}^N7YDQ`eDH%xe>d_i_bmM5tJ&Y#)8> zYTJ-WeZb`oi12cLSV+R%#P^O#qx1qrFf||Uot@8tFv{AU_>D_yF+hSFCJFZlxVfTf z7WH&&yLZk#Oy7SyR#BYc?3evmuN=R~MQedyH`l+Ylc(dWPJZpNx!HR!P)wS#Pef@w zzQ)F9ovN-?djiP#NdXquy)yz+0&Ehj-3s1Bh-}PWSUNz1o?t>CL+{zyCNlNkz9co@0BD^Uip@;d+l$6PJqMlU1r&|^ z0Vgb1h)jPMQ1ZS4R4_7OO|y|lfC+wa=BRk7@#t^#_tPWb*%&MJ0CZ}^sI@VFQTy&{uO)LC9ZEPlgO5&u zkxmL|u}BSqYVR}`t<1}!TA;*cR{tC^8asOPqORO6Y=;T&GJ=5u3iqGOeXfyTZR>&LF4-JdlYrI`U zx<_4K!>mp&UGeCk+DE_qb~IhRgJ9j0N7<)NymnYuIA@7*k&^zeQv~ zb}w6*ppDLTHcTHROFQotxwqC20oFB|3#meDI__%}b#)-iG6j4bq2O1-Axd?lHQ7Ru zUZV<^7IGXR!thYQ(a^HNHi4-uy2#Tt2;jQmUB1Zl0_V&7i)}pJz~k9gimtr5u1$EU z#bU=U=$Q z*)+nZ%kF_quy_(tsSirjL;xKZd7y~`(V$Q@C^8R~X|rm`6Wy&Qt3B?^$H&dwoiIt{ zZ1yIl-E7sNBC5r?uo&@3VBuWl zs5>bdB&Rt5E|dBE{Gf!=v7?vFHsBQf?6K@4DgjnbRditjO9Yp z@ka(gF5Ra64s#@K!fN7KR}n+qerdkHoWRg+S`eXP>f3MC!{WMG;z*-Oy}<66UTnCO z!3C5MTD(V~a5&L8%^qgZ52z9+U?+e`?FNN`zoF>qLg%W2o zYE1VUx!c8gCp>mcT05WLn)V~WV;1Uu7R|G1#CP?|YYbPxwCn4gi>>wGZL3#qH3eE; z5}JJfwGDbGM87&XXMe9R6}(_XZ9Q> z5~pDE3aqQZ^b8tIIs-1y!V!REh)1WPIc(UzG;-U3uHELTP*9KFoFetQcm9>D10LgA zw>0dEiwa{$!$!?7g&Q9gLI;N(h_AY`(>L>5Na-QNP_b$I`9p-XAdWDuKHfYF-UJMI z(AIa9-CkgY_D(kq?vS=K13bpIviQV7?LqU_;RFatuAH;uBl_|}vW%+BB)S`{-UXcQ zPcs}6Ln@+du@90$OfsPW{m1?Op`gywhV9FE!=@?0)0@u(@yD+Ezq>qSCwePLZ=4ji z`D)XL)Z_Yjo<)U%u)pXim~jKmw3h~~{3M&8@G*Lqlr>-~Q8ht&er&7;xmRC*Gx{X0f&h$NO*vwp^xbQhcu%3qI;l+k$Gi zpEQ*=-v}b`s~%3n#rebpr82jDq!aEJyoDFu;e>+G*F|^odI?cu6QBmd_88SP$R`F7 zU5T#UAO2#=Pv8r75{6@Bz%*AJbDIIGA6^@dpO^n;?2tiDu$TMm z{^`kr1QjeYxUR`W{0hj^)(*Z7%Srno8nZJ2QAIO}Y}C@6TcMQ&0(*)AYi2mqX$@VV zYi{AIO)~}hl~8JQSr3(!-6xa0+>-B49bIyE)?%Z5DZ%chlM12p zcQblHz}Y;Aw}D`*y3H>;hZ}!k`s2m>z5O1^e?Pd+?A0KTe*lu;tYYce)kj9KlU=Yy zm~i6HEtbKeiMVe2m zx(2M^Ztl$?p`)U4H-^xWSQD4$1bOJae8JWrsZ%D|MZkLCMSSwbtQ)i*sR`Xiu~6V< z#*@Fll_A+Hbs84zQjddbW$FgV3y)nEgQt`%jQFY_noT6C439Mqi{I?r3}mA|H~VtF z2ApNMj5~w4Z+)6cQ6fUw4PW9*Wh$4SjE}|tbdy7tvqo7oL_uWjhb4{pwbomb5x6Z5 z*O4D_?M}LEr3(^g{5^dlyE{Rfc&fJ+waH2@{4GklR(eWGVEGiqKnAB8dO^Nl;UKydjZmQVTGOF-nLNGv@-{N+G>lz4A6XJU zLFtP!8@tG|qxn#Ru)&BJXoRtH7ZV?Vkes=Li{^wIV(sdmeI_rm_P1C1dd)$?J)(Xe z8p*i24q0GQwgmD;r0Krb1WmWCIdw&e9;VAYFOX0w&-}=l_$`F!sfIwU>X}^myK!50 zGPW_hj@;>qQ-yz>B7x%zn#Qeq?421-qz)Yix&Ssy(xk*vy+S`Yq!aCO&$U&&^a;PjlZoBDf{b$X~R52@mdoZU&&byC#BndGrl##bx zpH$I#!S!)rK$i*R>@}=9<5mh5Dk$eZaak&#))%vmD;4{yH4yRr@ueUhJrwjj&v$qVZ zYHQp61!9G_h^8x~!73C=ze~QfJ zjuJ_BDx85~k@SrBS!mj2Z}fXgk2GSuK`u`)4weT;c|=89^sF4Omj)LXbH?&9v$Tcy zIcmO3xsqnQsTS8@y=06NJ(*j&;v5`W;Q0RP$#o{4?0(>XEX`` z$iJBq>ACRvKY~Otx@jfB)68Q?^XQ?`3B1dFPYrz53)qyaenvCCZ!QUX$JY*ef$=3cPg0Dtn&rK98*1T$# zsQeV?x-W;J4kX_78jGKH;G*-zDnR2ZHA{D^z!jSoeQL%4ji*7L%aOVhjHQo-p`9sH z=+rygJOy|;(AH>fYXWU4*t#O5N*jn;D{<&`W_VoB+iDO}a&&2^9?5EsTzIsg|PyE1m4m{{47FUX`vxHAmK2U5s3 zM}?5ffYpd8-!Sgf$Wng_tf$|=*|lq9C2hS&P(;+4m%}&yQuw}bn(AVl!TNksOvs^u zzGFr+YWfli?`nUzp=)=t5F@j`WAspTM*b^$I5*ojxCS4xr2|DAmaX)3&07o zV2EUk$7`eKw(}mBtKOj~k={s{&e%)da3fYuyZFndl{fA}0X7A=y|mMT2e4W3OO`8n z$htnmB_kf8rOASieP-F@epT+@l}CvK6raHmg5k`g!_l|!q1{btR!LCnvi1AIiu;|> zMdp1mF_9+g7m=>5szJ_3K(UXeNAVYS-Thq5yx?+qe>ttBpKa7i!C7etSCv2#Sxm%2 z6t#Wq!t0!kE8>&(CtXc^VNi)$+24JMwCI<{#n+HO!{NM1zuVjBfAqy9derNsAJ%9) zRPstG0>LwngiaA>xtf(|DG=SSTDH}CAfuO(o%(9mN-FqqbI<`Kca8y#)@D|E225XH zeh*)3)!M~8+pL6B>AhY#MHqeoDQFc%klbpy}9NJ*ozGKqPj*1 zzh5=$bvz9YNt|1kMr~8KU?)6I7(JBfe=^5}Hj3G7l=S4pq7l?2WwHoI(>{DJN#FYU z7M2%M*s>1HxZ0>m+H$b(8f971fixPDm!=UABsiuM_bx@57REH?tM5TG_ro?;1YeU3 z;OSbCYxZTVcC+|yf0gw!c--+76e$G*mcX)i+~Q9_>)D?{SK)Doc(}2far3X>+#YlH zFe2@HB0CAN73F@EA+qqi-ZDK#O1*CShFxCd-~|UZ^N`C6b(|Hdx} z84rhs<*^M*3bQC?({K(C2vYB!*R5sPYtPq6AMd=QpIq-lM3G!m_-^v-G2B-LE@vWj z*;y^|?jNJYc=Gz~3i|PNFZN;sDV(((n)eu!(JC|l9#OB6l}SjGbJTwx3aa+r8|O_7 zO28tb$Z2`ry{u?*rm?A%_wLu^O)=@3ku4^QzejQWm*fwT-YwM`O3y2Gn_vjgd-v{K z#M%~N5q5+ostB>PqQ;oc0K%Fw!URnGUUo+f==tMFsIo*g|7^13NfNxk8XTU(9XBwvt=x~_lR z15=J4dgCjJP{`64-Ge(VhhruwIZtW^IddbO`(UMk7S?Kge1gF42`W^$_Qkd-e?5N` zj0H|KZ<~>qU?Ih6u^Y7<|l4d!Ywi*jq=hP+maOlCo6E;I!oV@AsPfzxHwDf zE5_||6pKIUy!heJ_lGEJ7;%;2%kI-=5h-hu2PTWiO4Ti35HV6I0!G<+lJF>Mj9WU( z`(Q6#mMYycZ=19iI(-JdWfrQpc5L9mv0;q0v0G;wsOn9Q9nKvoBCh%xLd~-nBSEcK zC#6F>@cMkwvyCKosdiLse8fCU_UXJ!7rU=#+nC)!LX}SL32A+8$U1${4Ng$=M!f%$G9CLUp32hZnU0Ihd2dL;d%x&Yecw31TWWIt)m4DxJzilDGb;R2Ey=OHw&ZXn z!Xktko^S!7&}{4Q8$(I`W%3mo2bsX;63L}hJvhV;#kHb>b=A&2Wix3#JgIR!a+}9V z4wf2sFn$BixKIw}yHXRITPR$-5>0ZqqF|qaZ(Qc2s^+duT38UMjaX-=;POwmeQ}KF zQ0ZO%r(_4ZzaN)c^4PO{mP6qL;ei+UgL{7GInTJssF9=Sqc zK@fmJdE-o3h*@mz4r<8v#FNbR` zPg??q@1l^OruWq!)N)Lj2?OK8#}5W;9A8`-J{>OfxsJEdaK5JcBV;a^wj_#N2ySMR z>P)Pq*qO=bdwuuaDJlMQRr2V&!FpN1pYq%btf=;j#Q6GjLnc&xCJ8Gz5SA}AqPmP+ zqO_iNa~+@00X#CV@ebESJ*Y{2!8N7+kM@~9GxrwDB!}T0>stXYi8?@KZPapXpWOyw z)EKts84mIXVgJ);`{%dsj>s*bv$0S+Qg$2F@EVze2sG>pE=Fzz1jzrQwi;bqjI96T z)BaNm&}Rgl!olC!x9ULZu)v3&mbAORf6zZ@EXvELju{$C#(AQjsNeK5^ikR?)Az>rY7 zX8t7l`@H=#JP2((0l89pPV9#GkAM6d{*}qPfxnjC7_i)WV2L0P;@?~A>$Y$hWahU0 z3Bkbu>iI7p9AG5>@}&X(_5b^X0ifz%_TAn3veg=9D0bVWz-I!y&cEynkbr+V*=R7C zc}DlJ+fK)<5qJ*#+t>Zqj|`py3w3dWMSrj5{&|LQub|ujxibGUGyxsG)$D%j&^Qy&XlRQsXu@~62J!d4SGf_x1CB4Vlb-zWnVC=|K+$t zN%rtkXw_}>M3fv7xRL4jzp(v7q5N|`fRx2*;OoJp>#d_wCjT!KOm_9%*Fl9F1Jz|f zAYhlbUHLpCUI+78tQ)ld_r`{O-(jO8-Ho~I=}~#$0`Zs2UhylQi!J9s`+=&9bI>XV z`QGkbYe!8>mz)_(oyx6$BfhVZW$IU0+xaNX++%7O`p3M3Q+Ac(+ zzF-cjGQ2DxE_QVaeG*|?ms4_mqyOLSwmakz`ibfXdZAs;0SB4fpn~*X<->}XgZ1&U z2Rm2Gp~h*yYQTd(TjkNZ9sy11Q($)Uc(vbFFdF9>``oh6o27lwT=eXls~LOa30lI- zV4?i!r@h+3!^+X*(Ky?E-@eLcwm++QO99`+;WP!B{;A>|T{JFW7Bh88NgX~TB!6=q zgIIy!%vU+(i^Tzus)hnpp$Hy|FzAAHeg0CWZ(H$s0VB&FG@)^U#FqX3V<6>=03CK- zzu(WOvDz-ToV)a_vx!nSA=RT{Es?wj4%+BYv@rYu*NwCQoh{2Ua7$ z8)(4trOyoyBJeveq!VBa)6PP$KtI4GVLu)bFh2!zWy3|bD^XBc*bM~zw`DMq#zd!Z zk`g#D1Wr?sSCTpNVRRNO1BS2%6e@QSv6-w~PoRT@j z2zSyJl;Pe&*RDOs5TH(u}b8FcO5dbsYu21S8J5vmt^!^yz3Mz zt`xD<;sJ8iV{Q-U?w*L_aUkxiPT@U6c5Iis>C<-tX(&O6{QWe;tZybxjVxfXbKeY{ z-1IVe9LwAzH~K4R-#xt{XAmO+e{oU`~fg5O^3*#9Bjb*(HzJYOrYI1xMEm(42(7O~R95Hig zVT@A(lXC_BaT-$jWhc1F)qd(Oc)Ez8F~tHEEuYQM134((R&k zub>1mX%sVd2ennLE>j(rTunZLCb^(l$ z>XgF=2^!zgC^(ouWbd^WKcA`isi7DK7!wYaH2|hW%~{`2Ox43r=i-=##X6n;aMaCf zJYn_(UC8eB>ZN-O%n_L959XMt*{SxW>ek=r*Di(AriqC5$~Kka_dby90U4ly?H^nN zOpO{?hLlrfh|9S8WZ<)t8hUy7Q(5!OaKpeG>;#09)$d1be!h8ae&++TofLHfW}zDn z29zTo_SHUApnVppz?M#9@o)%$EoKGD+E^&Rm@WF%>*wUce~bd(=2QM_`teY#{$3!d z*XRyY7+&7X(^3aXYk<&oCF`F%O+UeGBdt zB%!AEDCUAhzES9uUpPjPbQOUZcnJ0eg3I7^?|bDY%ISVsBvuMKd{y$gqf9m+L&!W%R ztFmARpZQf*ThnK~Qwo8x@EQW_s*IfMJd(Xi zuDc_0sHl&dh{^b)dA#l9L#s(WYf6{wzOOH_#L61hG75H{5P$q$D+e_>h_GKdLY*3t zRw#w3LUqm-ICza8wk=v0*uMCak_*ntJp+rh3qJJ6I7bfxdi!73{k0~kF$Sci<=ksw zM5aTt{Iv7+B^qBhJ9*8YTnlq`n}7^1zTuCZLI4q-PH!ijaX<;LowfTi{VJ9bPb=WS zlc`_8;N@F}{ls@mfWhJFRv3Csb^lMhwD>47tFsUf&;;pL76!({SK`bhkCBs02>Tj< zy3w3Kg7>41h5{tZ@1Pc~RI05%4oRNd7635p?&V)8KzCfKJx%x*yEW@g1-H4$3-o$X zdH+f}=E~`7H=kwEl{_Y$5$Btr|5dT_j;*KA>FZ?TS`dn0-+);l0tBfpa+!zPx}vB1 zN40aEKNwcR2rZP#OWAx>MK_cB*)l3F-dl50B>XZm@^MNoV<=(DGDz&j1SzySS;QUY z85-3AV6XK~%WOdp1_2wQ$U|}3=I5GAuGA>Z9CFt;$GW-Ca0K&zYGq$0Tm}u%tc_+nXB3&Mc#v<%SjM~VZ=|eci(!7Q+N8etg|1l? zkXt=0W-!mcAM}vdW~RKK@Z)Z?Ov*=*deuZ*_I_6LwqG;0S=N}71)Z;Mh)1O1Va;pp zfCr&g=i?%6&3<5d5PL3iF4~J#(C51J#=OQ)?=-Xh_YM6?-PB$gD?02r(ZnwVcw{uuSR`er_`$4J;!bm`O2 zkaBOz*pnlBMeJLvc5#01W4uiZvQs0anU#C0g)qRm;m3$yod$fY9<6rB!WCrF>}!Pa zN06ISL`sifUNUA}s#^%Zd=BvsXq!Iif5hO+Y}rb1PcBx&R2c8|nw6nediu$N(jHs7s{-aSo2h)_Or*nC9o{$m8pW<0 zK{$t-Xs1i`EIduSrBg5q`t<^z-L_eOiqtl3e{}8|;_G#NzGSl4u(Wi6v(n>?vt$kC zW2Xj)^ie-XoYV_8qc#_4MCP!Fa3V0ePqil~doR#CB|PdLEZCT#}JPTMk0yz>7$w$Nn5IX=*h=lTNna2n+8q-5swAM2cq4A-}$G%wn z&4Qy-v)09dyG;&%DSd5PB%C)$-nXQ@rdH+rTv(KEB`%T}gI(;m%xR9L^dsBKkdRf} zB2nFYrStWG6RSvI1PwAtCPLU+tEyjSP$y|zAo}zXjYL88y=p(&>S%27D#Le4hY68{ zpp%|NgBwJ`>7vWTIUCWW_G56VzpReJV#2kfx6N<)C8sD`oMZR#d#N5&(?;g}8BP)t zp0val9++kLQ8WSxOW}z1D%^@_%*8FRJgCGZBe6o6)}Yl@yc0#EiKV~STvj83B9F<5 z&rT;N1G*HWC{6oeCCWoOdX~wgUKVZE-s6!>(_&f=e)lZwg3c*D+IAWmNg#rTCRu}Z z!!V8Dr+KbCN%%e0Zu+ge7ui@ODSQo9i_?{(1ixXz=^v8l7;Sz0+-Q{;f?gALn6hf1 zP+~~9+*9A}dV{FEv{4w@U%Pw9@Y92Nx#UMN>zV;B3T;QfNPnF&U`N^Fk1H0xRwW77@~Mke``}IrV!Mf`ZvY%>&PzQF&V3p{Lfl{i%lzeRM%r7hYYL z*)T@ovN7)z|2O7I{wW%(_0B(e^jjAxQc=dI;nT(x=UJ+K&1#Iv3Cg0@y`i@%mU1rvsA}?ssaxLkYh}aeUAf?$0=~5WX;-EPBz2Io!<<0-j8`w zZQa7Nb%CJ8$-0a^z}K75s_3s#f`9m#yIyc4w{W^T-zgZrH2HHi*}*0y+0-{4A{F4{ z$5llsxZ^7Ffc@ixVZq>~UB7VAUc&L0GrEBR)!s1uG)<7hmy*b5|YNi*z1(Q zD{r;#jXin*8-@@;XqlD(v2lRqEdBF$6xOzFt3OCCv|ZWVqQ5D(@28*+mG>QCbUm@l zHP=7Ff(IhQk`-F)qZFXuqnFAK4b&WgTMnt~{Q(cgJMMs|?>3KiO$bCBVe_Yx#YhlA zU2|ybdj-WruL>5u!`C0*o9agC7UXh+CLU13bIGsW%td4xh*$MPHmbG$q^|~cd81(P zD+si4J38%Zuo^*lsraoM%)o8x_Dil#Kdk*JG=FJLzialdsvEG$ka6Wz^>mN$)LwY( z9_(aT;npz{Moi?p{Rw|{0DTp2xnoO&JGaRb%Q-wW`SvS_|M)tf(9?q#A1Ys8Do4%&KoHhNR-+rH9#A1SIWR#vq}dwbIZ} z<3~Y-nBn|t?omol`HB4ZLO)T`B8~1G1^X*$^k`k2+Pld($m+SL->Le-|-=lU?Fj`dRf@3<3ptq2Pn@K2ae4%@juaLhGA=QN(ts@ zc89L36*HQnK8nH{grRz5m&11X;umA>F73vn_hOY+6?qVreo5vWzMivSUfu=Yk6^!aN+hkfo8BV%4Zh7 z$VZFi4AN&g)UU=~srmGtK z8R1=di79Xi>uF0m)^n1`3OHLYfQX;p?@;E|+lTu)(YZLu2brY2Ww+ur+vmSC)~J29 zdJ+c3H3?`M^A275;sX!eUniQ0iF&5?4w^01orQ>#SE6<4_Hi4+zmvC1N$>P9U*l)O z!Holy5tdb5X9BxREp?A!lbVZFlj{gOuE1Cueu@P>KHoALN?Qb_1OEoghizj_ZFQSp*zf9nu%LsK(d|kWs+VeP2q?C_K)AnbgIi%C zEnNuKOp5GZK5QAqpI&ZG-~zmha~nI{ukFyE8@wqg#2R{*v*Mi&SI=Q>knHy8k)+F! zuMgUu8AcXlV%#&iY8sr&ZSWY_q2A=m>P0ia_*~WII;`^k2)Xx(@%kE9Kvpkv2+wjx z8aX@RvDvXvVV7Xj*b}A$2Ae^5D*g@Tth+PT4VW4M*@WnQSE<3dS#<*j$N6R#f5~2L zVt_a)qCXwH5D76<)Z`glI>g8Jy_cVfDTbP18mjZqUN$$Y*)np=j6dRi*yp#5!$U$k zPc(?GKG0iw%&(j|5co-xK};NHLt^}KFj~|zQ($CC^<4!fWKwUhp!pP;jxgYNv5opz zw-AY;g`@xBN9yPW1T_&0rv-+?i_aygXeGM^V?NxZL=k>64`&2Ro>kc$x~-DjM`ztf z(FThpmZ6)w3xuIB11y|fmgPFK5fE2H;||qv z=Ckya>Kyq19|3%=L7fx~;wmb>C+1C>{cviMiIVFtdT|NZI#|gMCu6r#ku7Y`3=b1U zQwJzL=kX&Le#Lv<)Em%I1_mg{`No)>5QshgLJRo^qzb)O-qO)^N^W9(sQ~{k=Mz~V zi%?)D*|rsiMZkq62RjHe@E)A5C7Q=cS-xRMX>?u8tLhSr3>*?ieK*#S?-#J{msBh5 zeeDwn$$2W7h=|}P~k5|+O!(UQkPt@$kdX+x8(^aypD%Osk2IR^r!Wk@Yi%E_(x?b zve5JjxqN(W-lBn~_mRE9tp6$w*=;EG0F__obohhIW;KUy>jfh1G9r)ST=HSte8NV} zzPUv=d!$X+W?fY96S(!2t7z~2N4$BujJ)Fm&b2-9Ro4?jBh%;uQ*XC21AAODRDO)g z>_8ma2(=Uj+xM~p4<-KOTr0ILd@bYQ=HOZv3MZfWip5{dLIJt3I*?a;XFu-Oe-6p+ z6iaC18LYTamhi3!+lV5JGoSCSVmEP$D|k(@FwJ$7px27?r!msL+pnW?_8^;#^9f~< zQ>DE};_JP2nL7>I@*|}GWBRAh#{Eu#m82`B2OCcb>co1DA>f)S{G^=bQ!>_-A^U^f zPsd73;RVPe;^65wU%G}MF;pvj=enu)E6_rks6Xoc%C`lx?LQirw3%<-5qm-hLflv| zQZ$wu+E-N=m9@qx1t;Sb1anz@Uq`ZF5k2O`M%V1~_*_FZ|3h_{DBakbyZFaoyonXB zKl8d2%TlZ|+(4wSJF^xL2ix5vuk{E`I>!9Cc8(f*EyZY&QhKZkLeD==p^W)uTbc z>e^x{UtG^v>u?Ogwbi7=CYbCj9y9Le5*|g0>}xAN!X=A1aqHc`GW?aAnsi_Oar&xU zJD16RGW2JQU&H!{5S;C^?eu#;jUd{`iFx*oY@@F*C=!UI)nb&CM#wft`QQvqTwAGH z7OXFj$My@3s5Yxyb5`axYAQsIg54ZQSg$Dpj}(4DTYvQ0=yL{RQNUqgLn((@<3D4a z5S>2Sb}w_N8LzX=5IVL?ft!213L)BOMszlv_dokBlbKS4Al9@|bnAnM0ht66+6xMa(X?|mxgnF)P!eH~~kbderxXMwBrneVaE*Ag<$ zvjflE27$tSOZg>CvB#Hw1S*^viQ-)o*m;CJWu%PwaE&o+jBsA3QrGFCv8opj zV;F}i#`)HZv}T?6d;LGg{Mf(DdL4*35ZB9jl^D7c&3eJP+g$c)=$=*Mye?f6i=_<2 z!Hu(>fznqg7DBL*=!|2qo)wQ9`a(a34j$?kSN|K4xdh>5!4*7>t2h?2?}af`Czp6C zd_B-X9RGf#SQX56+4@U|%I{mAdI|~)D`m)f^GNg9&cm5iSh~z1;4)FYe=fd!Sd9E0!jcPw$ZN4pkeEhuIyhNky1qIM=~mKE1}} z;ziSdOxcZ-+JSMlzEb+zEx+Uu=eL8fy3yp+qy`z!U=fR{B>r=nyl!co0^w8^DDx9{ zxp7tJDf5~BA-Yf9cgrOsg;ElXCiwZ@EMZoah+zNEYHLkJI~L#7 zk#sk%#Z_ZU1lOL`$^{tgXFCKRx?v)lvD1^G7OSNX>x62ayo&Ezl2#h?T2CyI=lnf* zSCg-Z!SuN_xC=SsFgc5cp7SzkzIW?)fz61ZtS9BYNjr9ugj z8P-kN;u*b+@1R{ooM}|Fr*Wbks4-)fjUkspQnEgB%;qzXJH<0_44WRcKHBR{@RMlu zL!ckmbhKKnp0YXTRYaa+OErHXA9nB*KDfX1Bv@d6XvI)8sk3oEia&!k5mm`F z!8+l`zS$=v!IY58gp!$kd#a4C6!G5Zz01CuFOcWsyl;hXoe0v z{^9aOrWt1sENBzsOK6UXwts2%KN>G>c9{|tbxDIW^Wbj3+DFD?t&Eao#0fC0fbdAC zJX9v$4}%kEc&|BnIjA0sFS2Hck2GYC7@220)X+22XQ%EEn|!ZNd#B^6{`NJ0gsC8p zf^zUaG`>aFGWwk-R`^TVU2I&Ce^hC1m|>*RU`{dl{*8P$*typv3f)HyN5FeM;&8XY%&p(6^4V z0~ef8MQs6elQ>aTJo?FK8h)~bO#-c84J(y+Xh408dVmSWFEEO2j*odaJC<3Ob_+ zkY8-Vv)IVO$vt3_A62U68L)fcfhGd7V1ZT!hiTuGbiCi9!MTt5M{FrVWU2jM6|cKw zaDi#MxWLY@;nx1j_OB1%f8G;P9d)#h?r_cVUcbzJuf!GIbwDtq$l3@;b&!J8Z6+M-i5OU`NWvKrXDcde-JhU*G#nggMmWbP$oBJl3bXGoE2#~ z+Ex8Oy%KG}g#FxACEDGGKi@UAXQfgVKX}z$S=_!?bvNfk29=IzyHGPom8kw_w(45) zO-P(4=P{zPfaQ)JYVqzS!0iqP1{?ahRmy@1A%Ac>JNEF4PxXb^?KOV34Grdl3(CReT6Faiuwd2}fsn!!3wxdx zK4oNO6o>JeKA9C9US~38B%DG?JWf>AH0lF7wlG9Ke{7}rUdo7F*Z|W;+-5@_y@X~l zp4{rnL)_r^S`KNdpPxg_hNbQFoeC@cj~Sn(;L}&JoXiEb)n|QGBIOdlAN)zn!OP0f zPDb7`W>bYVf{@h~WvlPo`|r7tuZG4N@$>2072=|zIM*bJNdt=zm0Se7ioHVPAE+g# z6h>!1h=*Al;(z~Tf|XWbviIFB6{L9FcIff) zm?*qBNjjs)7RCe`t?4u`2X@U3Tpad1HnrYSoJ0%BjJIYSV~((C4PnTs|9Auqk)VWc zDGck6^A?TrW$&v77xTS<%@2vk*iL3H@dgvEi*!9*olnFaE3#BNkjr(j4tBGcm}pd& zcFDSj7rcUEh=D^;^Xp>JZGS&G$u_lcxKd@3GTG_aH>GG%kT!ZyFjd1sxN2c0c6?6) zO(Y${Zo+!uQM7~`V;`l<88GVWTlhqa*)F|SF~j4`p_bvCu#Mcd2|~51%Na=JM9-0q z-iBRYa3FNfFvoIzdS@inLf$Q_aER8)!7V$fEnRn*H+@aq^YjqMEct~FOHw89fUqSU z>9uWBn$b_&>zUk7#m|SaxQUMS`Ew4}Oc?wf_XMR)Mk~74$3W4Oj+2P$INZ$ z7sLnWhNTrb6tW@1J>^eDoBqNol_DfO-cSEbSZHwga}jQDHDlBul{mt>=&y(RfvXAf z!sb-}uH}r1zuvK;iATqn&a11hg;N}tRe35V#~%Ib`U@qPHYOhYRcm}z4l%7IdTX!E zrjJ8rj2+oGL13FsBrwKq;YF6NV*ES`L7E~Zz#sqpc5BSo~%rhWgAoaUS!+u?|ZVo)!C^(2BF zZ#MM>C)CD4v+!|Jg5{kL1o zW|yaqeN>8?&T8j3m(z&jd&zXNvkB!(iN_15W&A?v!s~hpN8zPYxlNWZXQwaxF_+IA zuAbM991?DiOtM)>YOxm27#9R2H&$kAqP-}%+Vo9ctkM;8h)3|2(~Z;FM_EySDXrB< zIG`PQmL4e(3|RB!I~^&Fxgb=xvl4@}OSy9u4p^yktXC%3EZ!24aVi(hB0`#$L()|; zj0;BT&K(|>&+g}6gq1eRvw5=$gT*URAb*VA5uWj4pAuvyYhGEZWvZ|4y%IN?l)rzraaDhc2;bV1ey^ zR1bE5kK|D3N84AWywghxInhdJ4C%FenlOA^ixe|yE1yYty=@=9i3n@~s%G0l;8=ovBk*ZaK6m3f2wvTd!`!@L%Wce}L4WV`5<3 zxz*xtD0JIaeC8W+_y4^wfKmVDxM?i>$o!wb+~$k*eeuTau%#|m@mBm68FeU$JcxYa z#jS|7bI@Y3W>M|QgT`04pDpen^_CXK!TVEBTmElH+2xGr@mm}R8#PRU@4XqYzr_OZ zkRWf%*wY|0T(b|9y&ovm+(s-N2HkJAf;^FH7P2ytm}0 z+;65CPirmVL~5Fy|GzJ~n)GYkdpzkXEP5L+GRvR(nE$YtxYb48Fi7QHKL||;_Ybk| zyCskTIH9wBm**kNEXV0;T~?$FN1BkoMKs}#BE-UIelQ_KKE8PCS>x0H%cS_n_$!}V zw==6f`^)=H#@Y8afEX+UKi+`NAEmQ%{`a>9BiW${5VwEX6@)+N|6ITI zWT?r4Tk>DNEbv|Ym!m$0{kb!9yW6b{_RW)m&nH9WHc17zsqHY*Sak088sB;i??nJx z;bk@Vj9a(>JZJU}r>hz1L!6HvK|dj{#&wl>KIQ zd@|p85RGY2BJ4>j`X^9%Rj&xZqcyv30l)52V<>Wb)w1u{m%1lQ6&N<*Au zXGWevoOPXHm1gr4A7{;j?AW`31srLe&J{k6B0HbvX&-NOSh^L(?B(b5-ou`~I-|gx ziuU=B_CpK*{goszKJF7ekkn`TJ~u~BY~r^E>?hJde#PUz?DRwccYNba8G|i-a$~`A zV;E+(zuLdNUX+Vd-g2z}mpebO*?1Kr&eY3eOQY zzG+Gsd@Sxx{Y*h`sVVBcbbTuIfX9~V{lK$cHnV+S{~u<+m<0!LD84stNL@EYDBxdaEK^}F0RAlly)JIx1(dpLLdRrbK-zAm z8J_FgNz?DNf=)xo5FVA^{?_g+xn?JXz_|b9_^&$8-|wvNs(F{= zWM>@PeV6p8vY6iht88EvFHMYjLpHLIK9h)%YL6abN4^8h_D$~1HiPsZ9=vB2=0T{$ z7$C{`{!NNhgRWz(RU1%=z5M>kuX4H&W%cUddRmy^^z*xF+%}2VD9E7S~f5 zsM4s?uI1RQlV9~6zi5j~bA072b$51jELGev_QXfknMXw>dLHK=frnx6I`l|*G#>(S zgX}0HV?Nru-p4+fuO3Q27|Lp}zJmK(w_Z+C1zzm^9(HZZ4ZK2(JaYmD|HsF3&U0l( zO)J|Q{69yK%i!bb7oj&^m zPsh7NX<_=shlt%%W4A8Nt%D1&NaG!nJCumwZtiB+wl%Vr(BU$aC`T>p+)Ug zG&#S=qC=J)h<|+wxXV7hhN28BKKCUSo#qzi+!d6kD0ii_#ZQ0T^#yhmbnBJh^W_Q1 zxlLcY|Nh>ADURXdb@Gb=zJb-r4xep+6cIS|_wFutZGDUts{*Xyx5iiND+g6G7$#%XxIEv1JU()>;*2R&`)oF(2K|xnt(AitCm=H24n7&8q}2$RGWpT+KOoB zBZUCSd1-;|%`Yz!I)|N@Cl1r!z02w1=<~s#=*3r$Ig`{20M3}<{0uKwLp>R%SGpq? zig&MsSKiRi?`rAZG(~th> zP8E*Mt&V-cxQ=)J(+4CJ)F;RxzGxfcMX>~5A$8j|2Gm9lo+c9`oZv zez+Ez_w&5*{W7(g)2@c3uJxOp*0_TrD74~d1#?6k@5^r(sn;MCZTOU4>j3CO{B zZ_xwwPk+Ydlaa*xl`B9b?D@X_&AZ4{b`L0w^3^X%kIVa0P0ihd=(agN&&`?ILs11M z>8qAmWW@wzr$6n~&AUYsAQw-CTHrq9`=qNL<1z^Xd<2g}-RNs+t>`p*2kGIB`|sEs z5$|9saUhztxGA?2!7|Ld3=2(w7?GO&UgYKFr7$6-Kf_N^$syCk#gD-=?wLN{>!n_J z0x{vzt!OXk_BU+Nch|vThp@Y^fc!6asm1vQNU871_EVc*H01sVzeA_yKUiWj^&q(a zfDrJFA0HzW$t5|p#qeMclCh7-Bzd&OE{@tRu7EsCw=r6vSv~9lY;1hoC|w=*hCiOG zu}y5!em-0wdf18OyLMuZ-8|NgCRM}_{8T@3wl$KqhkBY0HZC~(CzB{6yx@4hiP{5I%DJN6Ab+8l3zD&W+N z$;sHQpF!o0TMi_M21M4M%z~KhET#o8KFI{Tfg{!*kvfbQ*T96w3J9`F4`QS-y2fMN zG2R(Mb{1QDyk4EGx%MhB@pVqGlzQ z#-hnLuQC4XM*<1Sdw{578m<_cOw0GHA*eTsU#P`Q&9l;&m`*1mF5f6gV(v6Y6cH!k^7VrRVl0ZYji~78GP7iiVoJ`f&}sn#W6x#e$3tL+mwzSVUt$D>oQVMczm+y-!E!$&B5LZu0Q5o~lb zU^=)uO`8bET7;2jKuG=OlEAxO!nzzWF6(%#BMMN1U(h{kK6g>r8qs|TJR9GU&&BXelCEBJ1tJtrcNdJUHpQCz((q@b01;6(iMhq~b+UD{{qZ@t1>eMz8mGS9$1gX0HyELK z5qSO8(Rgl+Ym@}3YolvWDbA%wJ?xq6rNrrYP~N0yo9_<(Ag1w5rW4Id)is{MwSwE+{+sGcjm2nzQxo|Iqc;VNq^v`>-Mih?3F`5=w(~Nhl&E zqI5`ubPp{JQX)u535cL{cgHB*-Ce`X5JSVa*!z8c@8f>IeH{KFBTn42?zOJ#yw1o0 zuEAZ{Pl1-?M@dgb^T*=;VjdEj69#8eE{RSDV{1P2{As>`ghp5|$5axRK4yw_y%n~=1RV*l$~rV~ljgt4AG z&+pr4tVi|?jC#@&M;cjz(1Rpw!d-7yE$%joFy8-o0g&k2OCSBUo|3J@ZtmolNIr?r&W_vqI-#J8 zl%j1rJlXmt{6!~7?uyDlBg?_7#Og4l^wDfw5VTQl36DmgHda$OL~Y)n)c0w!2B#18 z{-`=Y1(JzJ!@d37B%wr5+0mV_P&_a#!JSRh}2?%K*vb>0oPtAULfsO$uHG=oy52X~L!FyexLSL_Qm^m+jaM07FI-!JZ6(86C zNR%43WAT5s}!IBs?@SzWL?%g@PKOp5B$6(7Qep~`Z=as6Y5so2rV+1}g z^c3QvG1CofRiZbIvg9f%buMdI9YvVYpWL9={Z!9*eEggJyGpt(=nTIkGmV90FO`sT zbnz5YEFVXb^L2)O3QXhWxjp{;IqYFh%F-URfq*NTsvlyj-yT3~-0YsGmsdMlsMT9r zDesV`g2fRQG4N_vFrDUz&9Gx8dT<%-)ER#O@y?iCXixE&+r{kHpiBy>wXJ@mkX*x_ zG<6TFT-PeFRj&f#<6$PbV>UEn`7O*tf9J-lOV9m`eB_>#&Zk`6rPVeA#yLAaAGM?jReG!(@(~na5Nslc*5xu?8!rC{{sm49F@nS#(chn*( z5qp5oxY$mQUR6v-Fc(rwY^PK2mEzz`bhf(L3kwNKh6TB4Ot=5Q$Owc4E^4`6T^xm| z5NSmvn3;s+1}zox>wD;ure#aq=S$gnV_sQoe3~;UKYod?ueZZ%d_izqheGA|(jG1R z8{|2|ZpY7CgX0lyGeB-UV{#kWRa)&Oo3pu3oQl_8o#B_W^2?BX?3>gly(@%tcv{#v zZC#OMjagy%-kvT6raO&9Zb>B77h6hyB!aWXD$3OEx!AMRCfruN`HOBJ)M2zRs$WB6 zYVV06d!JQ5ZJx?MnygYLd}?g9sga|2SKT{VFt@hm}(%x@2D$FtnG zUB~4RNq{KW^r=OZEKgx9#v95!|4?{q5ynKYa(;;=s{awO-I^X&+0hLRhZj|{U5fRO zVPtmRTdyjOo5HZU8!OySv?63z|G3Tl4i~9g0di=$c7nvFL1e6A7*nv4WuASmj_p*_ z49Olz2Ss+QN}f_(it3jlx1MBLp`dcUBniOAyUZ~f5#l*w}bMTM=11oKm0imMZp@2ohzSPgPh|394vW_OEs)^_S-7blk-5bSR2|fpDk|qA_hGu~r!^NGRnFd({j)f#F$$L>w!q-%wL;;W$ImxErX3dOOy6I2 zW0?vDC2~RXSd@MN{O?QjCiKbb4#w*YG(-YnZq`_%x2KF3xY2`c<{M{u`S>_QMWrI``uwVvQ+8R|`!oWDFBZ+cdl0R2l;FE1b{INo@9 zYUK&MAquL)9G6NccuhhNcP58{Ot|uS?b*8wfPh?E2wYqH))tk1MAX;ms1Px0_jI~) ze1f%DiD+uiQlOTXI8bBeCouaL?>H#EJlv)$oDw3v&nXkS-Y<)2Wtq9aU{_wGBO$HW zc+`{A0(LX+XqzPR5WxdgGQlpTr;)F$kl8RXk;2JYnWTpnI3`q_E7xW$_oAI?+>I~q zG*ahg^g6}+Im-kkd$05iJAMaHPQIr(wD#`9twUvC2$=sSsiI|=1&3hhIf>qWvsd?I zCKd~(+*KGj!k-W`e3Z>8`1sNJpx4*k$7`+ko`Y~s6O5}{h0 zCn(1BSaKd)c22t;BNmlEUc-Jn;7)RJ@g2;3x=XWo2E6&5SRQqXHnNX9HH12ZT80vU zPPKehB<=F$Xa-fbj`#DZnOXcB_|hbt>L{p7oq>D4fG3<8Gz_=8ZCeOGoJ< zAKYWeRWHC|t^~QfnzOYovz5S6UXt@(e7$@hc9l!c2!w+t_rhoaiA`&OCI+@eLv`M@wn`t!H ztjN2(GuxxhfP|t+-0i9~RougvrEZ~eveLpd4R&{GgJAsP?L}?&6fB{tBB#^2V))W7 zUZ*K!a&YnQjh#63$xxcu>o;Gwa}Ttn6lLu9JKKDPWMe{#z{ykB1mIf|LI#Kqz45l3~ z&~pHaH6|^!GoumdrcSXyZ~R1baa(*oOX+?G&={*RADe zf>v~fo2!Q)L7JsUH?#J+4!`rp%XR+tlOIq-$9~v;kwMekZ5ok*F_WSa;J7sXS*JA? zgVK0Zt&6y=?NjNR$Pcv<#x2KsFZ-Uu^@>uM-kw53y{ zo~0hHg>Bh)_9~?enDo8RJMS5%Zl^hyJgwnR5*{m6lQEtz<(9STa%&C5?rQ^x zJZ>3ijeqM&_WZyMm75_l%+hNf{!lA&P@NH^KSrMd;pw9ds3K{_2O`cpQZ&YpK0T(<6F-zy15 zb&Q8t^grBt-Vkb9tX*mnwyzG=`6wb{IrM;~{p8||W~8kf_wtv{vF>F`_DRI}vjtUG`>OyLa5}O7eS%6s zjp#(vh#SpSl}xGa_3qy_deCG!lT6mnubGKjj-Xe{*UI|hsJHmIAYY@q;aLyy% z!vxQpa9_ALO`aahfh%~W!iI&pmBuv;^0Yd!HrKCx`P@4GBy`%B8wb?5!#I+#JY@ugWwwBld2^H(FHm&flg*l9&_#H#l{ zE^>G(Rc4!it5X)kD^*C~JWSTbOu*C=DHmJ#^^@kN81v?n(_$`>$A($^GMI^od<+~? zAtXjL9;yyZ6;lVK%}rk87Y1lPGZ%_Me*N$f8gQN>>@*X=m8wzy2GQJtm}%Qk1hZ0? zUHW*B?pqfhh}jr2sVor-5sA;M$%7G3wVk~-2Nnkd)^Yu@Ee9=gq8K@s2sE@A?_*~~ zd)fQY{MyV#e)V1@2CW@&cj|B2umdC!==>lfkH_B z^Vq&IuA=j74z1dD_ZtMA>iUM&8oCZa`-qI;4;%i!!Y_bJn41wS{!b=oth*`Qi^1?&!1zxmJ;jxJP9>I}@7{X=Z3{}=c#a`hNYSnwZ$ie1fZ)JVR z(m4D>_VtRyhlJ>nxUDfRES^)1fyFRV23cTNei(#NK$}xoTsiwisHhrG{}C0h;AX13 z>rPEW{!xaF_0;-P3_7t5?Q)f}0{4?8yUcBlUV-*oB84TbjLX>pPZWfE47eqS9-116 zwVgwvLUm{4u&72@`qW-Aav_eObaX~DIED`F>JlJAhF=sUD5lDhQA4J(m+m?cs`?iX zf;j<$WB=(gK8y?xFDnfq)nm6F>x#2~BzlNNkc#@@8NI7qe?SeXGDgrI5@va~hp`jnAsot`%HM=ecp4$6LPHub3cNF;Ec za?^Ucu!vU&w7jYBzvNyB9fo!ac+Y*8_NRg}lp@cd@T?-8d?+N1L6NK_n0J{x%r!?y zl~cb?c@k>eQL}W>xU&BJ;0h{AI+qU$ujAQ33}3AExX7bj#kgDZW{$q}AMQ~I{dzVp zHig|NhKG>T{)R`}3D4SvE7#DKGCtVfc_I2_svbX6zr$+0Cc#~}oo7Zy48Ai{F+8g_ zas`2TWI9Trug!ex=%(8ua0#8Sft*at&L?o6h3IP<_MbO~Q~Nx*<;jB8f_lb^GPX0D z;y14-?!5QOy7$x{<#Ji7dv<;n>ADpYi4*47hRI)ktM;3AG_H%yUPNo#)Uos7!@v5- zmnGQG|pC@&;1F`|8$FU5J^85E)(ygrS%#%^AxQV*P+{b%M49eX%{Vg61S`5W)FGenQ3(-_t`NlAI*cc>WanHy}zUP9JHgo$nUWz*|<;&*73o-<*1N zymwnb%9Kcq*jx=^$=+ro>ePxe5Vwd-ggngp`0F!Q>#NL6QF_iJi8A)x42Y8btS+Bv z$ji;h670>dLk~gg5J99_Q(3HPN8^Q%VhJ6P_&k!i%>bh?^)qKMm63*h1Me__0W5i^raT2>Fm+Xho5F8+49T`B1m1*38&$R5KDZv7ch zo9aGaVG+ONmSw4TZgA&%$R#w$Y0pKk$X7^ngwb6tI7fuv5{B}dH`xni{cz%$ydO(* z26hl8qYm*815dQ6NO1Dc5|Y zX*vUYMKsaYZx%H7OxIN7To&1zx-O${P-D|}HS_n;;j%ChuKPK-cZi%kw?P#tG}N5Dc!4I;_ejCD{E|KjB|-aQ{5d%JDf2g zm}|3DT%aFsNJSSu{&fxGd@Ut>{xhGv=)(!~w*XuTJnB&5If&+xR2nAc+`wQ-M`Svt z4viN9Fa8Q76v}r8?>0@QQN!Vp|#HL;kg6ND5iV|(5rEw1&q`y!9 z3i&gzo&s)>TVfk5rIRgzw-%FbUo>i2;mmzI zb6Hf`>xVcnK8q;6p76Q6-;iZ|G%yB1&U>~6MVn5BYe#52ZFm^g@iV;KMsq?m7~bgh zLj}u`%!V|TuU^ON!j)3`is@&LL>wH18Q!CgY1*jj=#mQ<@oHgDOVEvJmu4%~Sk)XW zjr5b#wbBt`5q)JhVcPjEd+fUc}Cc1JZ6X*e!kUay^OssCm7qXMNF7{NU!br@}r}!nYj)YPTc3kCoO0(~K<@ zSd%U2yWsui4C++jnD=2sMmU?hVNCBBGlvgN*T$iBc0?69=eB`YUfP#@>SryoXcL`E zK?W4Z#$*&XOM>TRWm#pDLF+p%&ec--*oAq}?m2^cn2&*%gu;H(hHl42qdm=*#L>gF zz;jzBUSBRgA0#lpB%SK*4 z9YTs6h}0m&Tf(vvKc!5S zv}@Eqfg>fvJuA%|b!2WF~W^;Rq*d^toabaF{7#FB550O(?zIcvJEjayC#T|8a!kns} zFY1Wkf)CWsN|@|&z<8J#g2&|J-oi(a7Qnse9_mW+45BdjJ+A$O)%bErCTGW)_ru@r z#<188-9{MaJ*ejT+f?&_#IA2#bJfh!T0B>0_?Hb+vySu)6@Zi_&~_MMgG`e=?-oH} zUkNaQB`c^}bBqcRk!BTYB40(GNlVl(p8VdK+fP`RY&$#js#NSRT86a9=_H#~1PFAq zd&(>&I1EBJ>;6K&pL8&Poz!4^ezL&P9zbVzuZ@OJ%=s9T{>pVGnx*s9{+JH!9-2Nx z52HrR)EEz&o96EFLwBBAEunkSyN{ox2Mg8&V)I_!m8PMwa1L}2pb@A~+-QDzg&%~? zM!yq+j3XPM-pBiqk`P5^M0!YMC)WAmjwIc%{K8^9ro~lpgd2IPwn#LVZX;h0rB`R; zq8G%s(rUaftT#}b!M`MJKtsB7;@Pq3L|(YyNlJJKVS+k#Oav(dEgC^xNj}O zN#evYU7lDGRKi26#BgEhRiQplhajz>1ftaNi=~n~IjTdZbAd&=YIGk@mN)J*@rqeJ zlqpQwGhOg*jd`fD2X(vK$K8T+17&Ixq1JwKJ|Pua62Yc)Jek>t%N`BOxX5$i=&YG#aeh9M?^O- z-nO_RDcSm}Z8}~v&J%^F$H89{oa(flUu`@SbW+@N@l=!Ph+>)Jq@<}@E*FO^O&fun zqqj9h(59+Rg}2DzMnO1A?=Aig=PJ&oQI&`obzRKGfz_<1JDGNh-=2pA%iY;@>S>Q= zrdZ~D#e%PjMSeh%8jZNvJ$kqsi;6(-?a3rBPWvHD@Iv|ZBc51b*@b_Wpt4YFNZ_)e zJBnZ5if+lm5>QlJY}g=s^}bKCX};%nw|L_7IED5{ViII%chG2QoQyjo9BgRHVkNGp z-^EP_g~zLl`^mYH2>Pz_7Krd^#@+K0yPK@$&_Y$dkX@x^3&gSQ_TzHj z`cmE{CRquxvv85f5SIQ|ff}u;C|bif$2veTo#j1@od0HVlj!?-u&M@O#Wt9`ck0y7 zlkRE7ueQ1%NVlsd!d+7MVuGgChL$jBo5CO}Y8sYuM(zPmb819a2~&fw^k?=%=DAM! zRO^&=5_5GS1W=b@{kIV4kU*y=?$qo&>HL^|)MChL)7xZ$9Qz4@={}L~@(Ag733a|F zyZ7kf-xjKq!tdxem%^r;bf%CYkIT2OY6lYWrT-q5y*$DCrB>)4mqfahNxSQAzgocK z8)_>iHZb{Q2P>S8c!f7E(vjiGF&4Gqe91R`eE*H00!YrP)aQYxB2f^#1u0n;)+zjA zZfPTTdn!r8dZN~iTy7KvwI#ZNUC#@U9-LE)PcLF4R(*CFnuE4PYbXoIwvmtoXW%@v=va zkH}RhubAR8#=7tjfnmM)-OfpA)Z$mnbK zB+DgjQDON!dVqDh>uVy*$e5p5@KrXTF=p}L* zKgG6TRh_i6S3Jm}$Ww;Bl`o=??}%a1*ypZ7`(v&y)y5ST>vF@H=^5h&k0Zx9{XnWu zA+jfLY1vO{R@`FcrHsg32nO$#OimwI4yPUmKtg%!#eB(**kYF;`>>>|K70Z!iBK_# zBKF}*52;^p97^fi1jRzP7ay>{e}#wSJs|Pc%jbH?wzt`)`Ovc~$F*Qfz0Qn(dy2Uu zvHn}N{f?{+x{UxDNhTHSp+AOAE}lb_+dT6-l^O|sLi#&A5|56UjhjKwAbx!A=;@(I zN-6%cVs@|J|13lVnrL)4{7Gt-u7w-V)=mBN^I*DGqmf|?Ros5VDX+lZrKv**%rbMH zv`;5~RoX){f1b4K;ZARSllkCP*7UF(jeC1VZ^UMpDyqH*c_=P|VSwqHqbnp(JEw*x zq*1?)SGf=Fx>#_q9WHeRB?z7msxSS9iCcnh2)AzkZ0V%pdC{tw{)O!>^O>QPp`VR? z(_4m55{IyKI}?gni9nd(dUrUF%f`;TKMV*xa0Cc!6`b3cmf0WA+Mp>>a9= zd9ZnZpIetCcp{?8!r45r{I^1ErxLrTjcX6Aq%WP-?i;iQ+@ta#%hgRrtBhmr=R6$_ z&*Rb|oAHMpTQ{gPG+~(MNd55foBQrk^^6CCQ;7P#CHaHO4fvo70oO}rudixvW=^t( z9~GvRrfz?DBQbb_u)JC9h@r^CH-zQ}Ib4sp!j~|ED7r}=-@FNCHjbh@u?yaAEIukq>!#m%sd|5|{ zJFFN9D-6PRrJ@u@(wJWes&|6ne`3Yy!5XTqwHMV|muIsy;y>W6Pc{%g12F9pTo?L$ zBId&iSTvrKD8j@hj6seaR+eO?T`AK+V4N(8KgZG_mb`##4a9v<8d3 zns8nG@%QgeH#zBv9tJ0=>tgvnt)cm>@bqDrzZYE>{V#X;xj%<#lk>@sUcKYZWIuaU z`$UiQAu23cR44Uqu7866*u=M@4SO{(sFG?a z=Z}yvoBbU2#1e}e^?Lhz1^d_&WVXEA8DQ*uR+!vl4<9*i=L$SmS7)RWu!2=GEEaw~ z^oX;&rnImspnZNCH!O>1sE{rpd^(cix$jGwEIYxzk2 zPd?3m^D_O=(SJg}1aY-oPCfW{Rz-0LV7ym_+;FM>*B6V6f8QC$#=!&oyOH{nZt?dt z%Jcb?G)GUYmm&Yk@pxj1{Qx#oGyGE{K|n*>-n2o|bO5js@&Nl{G*`8)(r(^1DJdyF zH5I1Z@&Cg|DEBzr(gFi(9MkJja;5!ZtMvB4(|=)|d|krFDyPGCB+d{5ij%ccKYT&E zIWZY_o4I=T2Ub{)iSuKnrV2+5X1!mO+jXjJQ%{=iQ3_PsoT~iW6B+@SBysX|bM+lG zidxxzU!&2qn>{=o~M?hY(k0CoxsI)pl9$uB$@xx7X1ClBTm09 zh{^9qBz|DLr5$kl9z#an3MT;jao&K-Q+B3XV)Qgav8+*R*sRa0aNxE#Sf_vJQvUN} zu08twM(vglfhEgiI-D#lIUkR%!A(%Ua|(W^(s$V&FLD#^@9lM&bMmtMG}jK|mJPwD z%zvwqz4Hfa`oGp!AhyMm8tzE%dd1LpfMs9}#v+w(Tl}BflGzo4Aw)146dg|yeXznx zqbk*2y)!A9&pfI(k*5vYZLws7AzRPJ{WEUJ-kyEMJ-yZrP1b3HL0MW;AGrY zk~XP{t*7+j1#hcvTQCbXLn<>IOYQH2>i@pW*J5aQ3<~H>Kc%GX1Xktevoi*cUM(^u z04{;pBQtGn zzc-wt5}i!m8QCVo$Ap#q0n5|3{cNl@4ACcgfe>|IZTn$GXXtMt?jgL;Ofl z@58^`>rYncUN$(IsQv3x;QRfQ`C6OK{dG(C)4vkKz=ESlZ3|DQ>>A|LP?-zarh@$CK;+I#mQ9IMk6r(CrFzYI- z7;70k%Z*D{Q37h#KR@eB`s?WfUB{~i50~wUlB~mx!8UB{**dqXXOR?79X0A)ZMMNJ zKRs5UBbcXAq@Q8qY)e3QYz6K*lVp;gBz}J0ThpR=yXJz??@ZV6dW+kR>|Ppj;?u(# z^QLuPz-Fz~qbLUk^rmTxtgS9;SHm4cMXq@%)cnV+bs#YXHr{mU(z9(dQ(dCxzWO<6 zHwg?jrac!hu^NkI& zfU_mwb&N>NiF#uObexst?rUNYrHizhRg8YO8CPEgbWU?KkVDLhlV)&4|I)sKTCzCi5T|}} z%y!1M=}M#V{d5kf#9+TVm1n%oS|24>SSX-RwVaKX=~HiOU)1vJYA;K4S288J+lnqa z%b^>(Ka@%nTAN?yWrVs|#u7pfAD7?$!N2pP3MYqr@?rUtUt6Q*0(g539X@9R{sSaV zbL!pk9DMpTbX`hmVl@LOAW|9ts9-(~$h!Ic&1<>~OCUXAA2`%#Nsfs6JJq7!8U6S@THyLJ%}T(!3U~>%MM;*& zP>(QJngsnQl64gEYrN;B!X??3Hg)sL!!;Y;2 z+j~c$P2*XvgBpL94}LEc>3(}!yjil!b<`>XYnk6-pm{7`i>Rh@hkr6YEoN7w(Ai^i z!T86IAMwe_+agG+GM@{NBU0g)IB)!K7(>M+YRhcqOS(nM6)Ul&u-Kv;<@9;P5sldG zMLc@9K9 z3pHu(I-)99Q#9y*O?A(=n{N!}?o1a%7u&@MWNR-&IPjyeuf zuhkU^bY||tL?dLt%f!vp*36fLl@F(I3%Tv;vdH58&-C2U(Q)Tm7MkX_LRy0DmoFsq z(epCOHl75lNX@6GaZ{Fhg4VUB?>=I!+r&h|ngE7`5`TrBm@kAonnR?JQ^!xO`B z4YM7&q?S(!IEFm;O9*bpO2UN|u_QBf8EAYc(>x$_jJFxqRMVF}lP_K+=sJd3$ryS7 z3AShySJQbZpv-@hkKLItj&^amM}HrXg?!cuZ>jc8cLO5*esK15=z{*EAFNegN{y@< zqWFtUUGASw|0pp)O#rHY;(T`ODr0tL@TpfC3c9pSm4=dq;P>jc@aMTjjv}IEA#4n{ zQcu1sB~R^4mR)3==A)rw1&AVRX?ISh>n0~u#;0PJ@3d0%3q6{BhmrPp{n9XssoBFTNT^Js-={U4)e5xU1tMutlz>< zUDvs$v@b@sDs7je5QRbUq7{AVJ#SrqDfGh;Q&vU7T*94k#xnw9PYF_U*RdQSpBjLp zK_AgImo`=O?&2B<>~lQAU(#Y{q(;r#HC^lgdb2~_!eAwvy?HHIH=s1NL4n}DK~Pkz z_qm;J=7>gDfK0F7tI(XV_b9O2Ohy-9pfyHXHjH-xZ%8hA~@q@;~x`1D*rSClLpevS-Zg?BP7FmO~g!P zjnD1Dr#;y>JT2La5;|E3z?%Xt3)}5bSuuR*+oyU`n)jx0xa#q%ddT{tTn|7_okIv+ zA^jSCmwR|0=1H(22_v2;goLf4?YSB^t~v zL$*2cLk*T}*XSU>;HS+|y<4|i%l7HpkwBZmooLp2L~F1H)zO1sb2g(|!Pa-enEq62 z;&UFT9dhi3h|hSwUI#2`CS;(M}DSV|)Bhp36 zF6^RM8c#olR|-n#+XS4Axfy5ZdA(BeT#mK&JnkTN=>ARH&(BC90m@s>a+T7+>KEIOu-Yd?z88PSYF*Csh)>^QpwPp3Mp-(8V5JjB>^MGNcN`dxNa{Vv!XL5! ziV^SRl{RU^3F)KB&{ss0N@Q!LoOAKH;Zs70~)?ac<$&J%LAR!r!I{N0f9F)B8cAs_rH4jUR5k9-q5< zRH?bZVmZ-5r_EFx)V~6uBt!<^EcbK)o`- z0OI5R=9zbO(USY@NeR`3ko_eYy2!3l{U*MDeKRS_DE#m5B%^F1C*9{A7oggkl=Vw4 zR}l<_nU4cWJlre`WX@TIV}-@s3J6-EWB@Vf2RY~ySl6r0|_8k!s<9q`t)`GuS&kA)z7w&8~+WGmN zTxnJ@(wUXuek*V*`20o>E#f9L(N54{Vrnj_zR!oGXQ1oYVZN`A=XPkcIjypm9b*7; zjgx9_r16fKIE)5@u=1$S zox^EsckVIGHahFWiZBf0+73~J`a>VDf^>}5{#ZK_MKV9k4V$XjHFP{?E(Fm{FHpHv z8lT~cZhniMNi+&IxVu5yjo+16km%6Ss&8F_>M)Kf8Nf2`kol>{U2^M)a`9`#)6Rvf zz*<6zzKaiX#B&Za>0Z22w~pB6)FX^Ta_&P@fK(!;q~|%FL+Z!1fUMv!ydCQ9BC{Zc&TcdpmFXwarId zT|&I=C`PBST@4iHE$qA-$0DgGR($EZN`{Q6X(9s?l#>N|Vg7W`lLU4xBc1(9u+=We zCGp6Iq;oTloe&~5tL-!P--21zBSpwV{q{H}OGoQ`-Yi@y?}I_*5m^)Cq3EPCGv=O< z{^@Jk2?&R@H(*gt+0c-aND9mbQXo$w-Ntnj6B+*?`1-mER!rQ2gtC3Lz_sS#zy7?o zM`PM1S4V{B-kIE&PQmNm9}g6I-`q(%#~f-KN*sC@>&7{~05>+eFIlRi)+kf4a24YC zm6|`#%;kxP88?s2Gww89?`c9v_v|Ia2kRlODN<~V1}Eybg(e>_0eUQ-*pcn!gyEHs z)o02$nhab5KXUD4^w=A3n`-SEFOXfXuBzMw=)rE&q{9O3)K2VNzf>67vf%r0K@o?I zGPuA*3eyRVAw)(gU1B!O0aBv6p42ch4!;%=_jZL11Yxz3*)D#x8Cy<$w(-FKcOATO z8-3e?ODJ8?tiPRYR0Z8{W7T}Fo$nrf;Z@7U?A@GiND!wv9=Y0yr1AuFzbO#BzS`+2 z%PLd9Tn{NY+#Q|Xt6Zxo*_}vVUS17=>0;7oW7$ykC%PaYD=~62Cil(L+?nfYid_O9 zh?!!qi>>cC>0)t08$on0HimGNFF(E-=3(Kw8HV0{#VI7f-9?WkoY(mik{WuSQ4Rj; zJDn$U%JavTN~1BL`{L+T&Le+&waxkm8I07he8n5c2-xJ44mXFP$Hvx@k{L<|?&n9K zLAV5wupPih?G-c{j^z+>wbmA9{F&S^wDA$Rr+Z*6L5W_kF(dm@7vbDicNpbP+KF2UZcg4v!p^gqF}F(5NR!0HwOn*w<2y4otg+1 zT9;wTs&c|(xRj+#a|o+%%sk@S6&;5K*5$gsUw-xKRZ56~Gi3~GuWUw}_44DvLq=5( zYh!ECz!D3;p8$U#z?)#)>U>1HN1oTiY994aDQU4r*yI;}-F}p4>J9m+8Ach*$K^3? zy!rA059%%#g9MlmFF1arI_U=Qzcy80nG6>stz~)fB^dPw!sM6EaPw-&%dN=U&QgPL z)wL>+kV->8Q`cYZT`bmX546w#(nyPU0cZTh(M^+Yqw8OllFPDP1Z|^%K?%BXYbdhR zq+U{twSjtoe{{F5HdX2F*8Z|FpN%~JsUC$){^hm{b%8hc$sK{?4{%0f>3C*p7~3Bu zkR(6a=Vx&^^$+jV>3(*ShXGY+3Kyf{9^oqC(%`e}elC|69am(7PO$!l@dyvDzB`#e z{~C+s!)PZ~r9axe+!yGN!dY{8R)4-K7_RvH=HGdP77?p4$IMe$kIm-&$FENaqW{D+ z?t&%rZRh`Esr>Jc2qGr@9_p7DEFPP+)_?r>|33Y}Q+@jdqudqA`(w}f-~FdqCGmZ$ zhHq<7k-|UhB>vyifakd1SZi1P?{7xZ5CFm#*rbh|dgvb*xqb-hHV?@NmlAeP&9txh6duSZU2#Qb|J0>iPu?>87_kcQh0!Z;U0(eIRT46O_2?U;XHb6mD25`ul2veZ)G8W=I*8u2yL##t;_6x#X zFl7L_m!_^G3t2nY>U@K%({Y}nGKb|(R}g1R@-q(@cbBJv^NZ(*tNXLOyu54I!yGdA zf_K5W?jR8O^J%GTf_ELsZ`4+L-Sm7B+=7KRZGM-ooS{?CdGM)gJAvH9J;53L1Q7OZ zp!d9fcGfHI)IZ=B2yhnK9{Dl6OI14_C2XR}a;=%%cChdMB^(m9V-pg+L2|N7`e}Ql zoQO-Lx%SX%QG@AxXMm4!qNynR+giB&>}jR_It9dQ{TG0w#!GcLt!;`ff7H#|`{e!d z;P~H#@Yjj7qk|^@#oJ|`I?*jrqz zb_@8{6ynlnf6IjtMwJZHhfpnzfrkV;$3B$2Gj=T;wTm;dQeTnfJ?NDS&%bh@F3_5O ztW#s5XX3rPW>aastGzvw^P@L4&2qYGH`F>@KUbd-VJnuKZOW-_r{h>z+pEnAw&4Zu4|5gGxPS*OG(NWmEBJ7dmn6{)scpfhqkTQ(H|f?r0J z>$wP6%ff~a^(8u^Y3(p+y^4X21rGyav_NM(N@R;QncGl0Xl$5?~_bU!YaiStwcb z*#^%!J=`|kMp);Ltl-uSu_i#Og`NUb?ExD>TN!E5EO4mG@@^#@y;&JcdS$fu{uZwN z_%ZMetV_Ttce_@{Dcs`O1BJ#izPUA}h;uH{qZ$Xg;m@>hn8XTLuRPkel z;?8^34}7VxdV3MQX^|e8OfAgpvG_GhbgFP+UW2wL+PX_S{_2z2?3PlD3;WL9-Br@L z-a8V#KJMCl+oL6gO6eNK{T~B{b|!9Gx=uHdkp9FBA{-?B$DYK?^l?SJyzB8Brb;<< z{96={Efz$2Br^U7{FBz)e9!P09LN05QOK{cVa;jy$3W+qLid#7|KMg@Fd*-SUn6H& zihluTGLP4UI_cZ6>Kf1Yx@~~FjIt2udAEV4lg24CVm|nJ86e)&h^NI_{T4HK3YG#k$W%i}e~ZOMfYLaWKp&jxIdb03P4<)Vd^% z#$0Dfp97L{nyO>a6Hfs!WJ8vFuZiuYItiE7j_jjd&)pNq4uRBS5JF|rey2}QG!^UQ zcRTi2k=4%&D{C_X`e2dfOYSG=IMeTM-uertF`DX0;?w(Lt@0XkqR#;|c(v2MEv*>W zaPX1;O+(n2j$H)+aD-c8M4nx^gV&BgApqpT)B4%+XlQ6CB{vk}4glTQd;-pHT34Wr z*H%_mo)Kx5DA|*mmY?8u@$5}%IyT~d67g8j9d<^axpZkqW++aCM>A_jI zd6;aH&zTUQWlR1Glo!S5H-0N{&r$}*RJCT5SA63(DcTxIcI@<#6 zimphP&T0rm`Gk9X!;9|J8(>=khylOO3Mb`G=Qf!~gBQ*+3#MbdS#O_sr-va83d^b_ zy79!1wvD`>tp}Vt6oe{;s``SI-&sSfBtuWn^XXu>`D=3(by8^Bb_*j6cypCj&G~eP zIYY=QE#r5ht{w$r?E?sZD%Nj(4!B|2VD9+7FQ93hLc}jW#Wn8b02+tgrYj!}&W7FI z^ogu$uVbs~E5EZ;?(p})i#5-ZB)}Zw|Ksbc1EN~Dw^dLFsX?SWq#LC>Rg@3~q(izv zV33mTQbHs|5K-x_p}V`gyPF}tcg{Wc96k4UzW=sJ?Ah_YYd!0UE;-CE(Qo}2WG|An z4i;DYDJcRXAtq6v`y(6-|{19EmkV1eN3j^o9#DvD~Az zWox_vrLNG$MXQq2Y2#P^Nvk}Fc)XYjLyb$nnX9z@d@abjr))_HTKCi7%wzQ1Wc{@~ z)*S`D+y&=9Tb5&S=lu8PIC+7-aJ`SNdgDUe;0AWt)+TL>GxZXKzLFXuqg1fPLi@Axcu6cHIvJAWgT~V8$qr7~(yCulO?a@dr8{)4s1jf406G zG_PF)sg;5p$f&U{i$&DCs=>OGgg21}hwJ)_^B}_8E@c9Vs7Q(I`JQ}ip}UFziUqTK zql?Yb@xhtFMbs)HrT9WiGN~&Dzgc?|;-ftdHIEK%pjiQksi2W-DSA zBI+p*72F73eQ9pdba$@D>hC%m@N9Csk!B`tepM@hyKHsL^eioT1heW8M;OX1r*}Xb zEfm&A*??0l!#4r!{DSCyKBUgQjS3YJ0p_9vZ9#ng93e{E98IT8p|eCwo#+AsTBrm< z3$ADG6l*R*IfA0A?>+~PP@}EVI7?E#CkzKG}M=h-DnE;U85eO zK^zi+ECYSIN%CGe-4?nC(S8U5)tVVz0*&BXg70_PH*@olu2PwCv%R9G5&E#(1icfy z|DGmMR$E)oGM)en<@LqfRiGBjb{LMplSlM(ls6dW?%`TRH9VRoWy1Z2l_(TMPG85~hOgGmwui3KkM5T^6(FT3 zeHC;50e=0=t$RPyNGvRu=*F*hihZT{!!C|K_ztB=UVg>?T*W_*MSl0`7IeTrN0edb z8(S;?xd>~Y`Kyv*#-oN(SB2-GF8YP`{jGy(N9b_;PyOa!R5QgR)Svq{?BmC%aH#Be z{3(V@Y@F-ecP|Nh5}`s3w;QmwcfDjIP|MtO{9fL_U5M6U5lHCIC#{8abV$&JbJ?fi z%0Pp$gbx(y`)nq)DUFD(V<;h$jh(cOGgIB0GEyOW5O#=th;vf|YL(Vou+1dq4qeS$ zS|vPe5Zl7ECJi2DXa@U+ZI>X{E|xX@R`h3`6Z?|Hyscb7TNb2B(qum-sgt)w+Bbk1 z<{7mIdho6U6B`92JU|}wBM+KX_v=h-PXimtae}K&*tv%|t42h%wM>P_hx7Ww%$_qi zuU*qF_sPlMzD?miLl>`0FA+Z*CWZh!bAj0)H+W;4|1|#k2hQ=29bUQT%-N-NY3ny% z?A7vl_&mpTbg8{bz&AUP?j7I{Yq7PEWqA7H_PK4K2+moqiC*z9?C0moKk1*O42*v~ zp!#9tu+E)p8EH_BcpdoAEXHi}?N<$%^|PZ30y&!FN0EuRncw4HbT(;{VmL%p^d}MN zs70A8ru*H$sST4oGLN9VX?gc|Vh$id4tjNMYrk9sp5n8&oY~uhznho>L@Ql0UuuL2 zR{+{o6dRnSnJpWr;(7!2opVO`*mA_x!~`~(#5VuMzvMeXeEn)3rgfi*UhB5^cQSn6 z^S3^Ux#=HtD7JBHw*e5ARtZYWE(7K%cNcTvQ`s8kZNvq3Y*A7DagH^Z1{>#iBxsqa z$fK&E^7)kJD$%LET4vS$$h53a;-!;mAM=uIh7_qeo#eRTo`Tu6x%oG@S8l0nC7ij{ z_-*@{N`VTdMpg1k$B(Rv2UxoCgAj>SgJ)Y23~s;?4Tbm?dE}ptXf`vLyalz}a9)>_T+=*r*nN_nNNHDg` z+1%X-l6$wia(1-2E)k0s7kVG6iVa86;LS4hVO4!-kXe=~76q(;as;|nABL2Z|I)O0 z-jP6#m%ma*VIsfE5O$3_YmXkWdRJFdfR5nt>yTPiJmAC@_gM7xZ<{CO4VPH4(Xbbl zPW#yfzuk>r8s-48G+Lwp%P^0z{~tg7*FU4<-U%%-*{?GG>8bVCfBbb2{a%d2%Ax2< zoUP#>jOo8fdA}YoF@uZra_GHs)jv$9|N15-*6V?G(ZwrKw!eJ*UtclLwkMRPz{uwas!m%9yCxAE=%Rf|N+TZm4!w2Nlx!7WqYrNDpb$kf8 zc$iPY%Laj2yPdtgWv$oEtrItomu;T^g3*bw+#ptF!;f~$?Ltq5*aG;VAW_gs;7h0R zO>wOKarI)0)hc^U04T5m3zk6J04K;Bu75lAAUVn(b4Hu#(FK6O#aG*@a~K1xmfSDS zE;IJM#^KHd|0U6ejr;kN)sd!1S8+$Y^}wXYG(F={z+q`*C#G?D@+ht4E{(Kt);FlqWq~BXU6h=c7d&|O3DB-5#*)>M z<4XOZWB(tTHMod?gi0$#=G^{rLt?KiH4`xVW~%YeXrrP<|NR-DhpyP3&VP7$p2_6@ z*W3Ni1I34sN0AxCi{CT;LaP69@LdY^0mAG)6N}-?e^oSwPUHs#3yOgMQf+^3Qs`_L{qi!6u5AP zMP`F`)igke?ZEh)l$EGnEORYG=96qIg4gnGgdrT?n$ft+qV>7khp$7FSnswf7Fq-27kT+ul zxWH_SAey8bFt=4x3L=*O!xoOYa(fzoJ)@1G4Np};4pp$QKWU&Jnc0zcPcAChLBU5} z<7XpsONcX0THR^~Z0aHBw%ruJa;~=rCiJ<~D_fXIpu7@bLNQ56RzUP01Oyu$flGm1 zS~y$JK39j1mdQtH6i_$kjM0C_yH9~wm~(jk$1?AEWxXvllln7X)cx+=yOam05|P?c zFLJ*?ytHzeV;YNj28yyT*m;JEa>|d2a?G8E%Tv6_ZlbF(cu`j3zE!w?zU)%q1S8`5 zfJRG&R=t1P0e`z)v5nXwDXmVh&ql638w;rKce3}qdgQzG{ij68IU!b4&?T=~D~#7r z@sowcMSiL<`+fk0>gJmbGN~4Vg?b1Cn*`dCe~lTJy0$F`=R9>2ivoU`7Nz9df!@cY z7x}rlJ*u>9?_tHD5rpM?_4FVS!h5@BK&~^(O~9)D`UAa!<7X5X3KLkg zvr-sT;Y=_o1xfV64!LQUwe{Y`SC5I0&h?c6JNw`b8Rg&f%^Iy$x(0+c_yGt3t;ae&6Ji z&64Tz{gb(J7{B~S4k~yKTt)v-d2H({AP-=t{MZWw+OXQg4}EIV<`f<~D}x!aE_C27 zztOg>zC6ot!QOFPa_*nza8Ea=#08XZxQkU1qv6g#Dn)&eh-(LDbE!ZTz`8g@g~Vo0 zC+xe+mNoEu%f24_io8!s+RC0p+Vzz{3sxo7V%fAbt6Mud-grDnQ9xRAlBHUo?XS=u z7zn+}1DQHRhrKyFt_T3(TQ&ya&3X){RK~-Z2R^8e&52)QLi*a<=2A8?3O1jn%&E7 zajpQpn&xXw<864%tf{a3+65bHu)Fy}YPr3VcqMf3-fx(ilx3kdg%BC|lfV7WOnz^8 zrw56qCKA@X1+H@z_OnJ6Yu&14RgbErPinpC)Yw!6fq^t{#lm|z486EoL8^pq(JNOq zpB+P-WXSOTl#$Oe)sii%L>Uft)yW(>l3P?qO^{?pcq0%SQ~F)$d49X)rPnE&Kc2P- zuy8G{ep<%5YbQM-1Ttf*W<$Yb2_djukK`Q((mmKoa@n%o zl#&8Y`}CX0Y?ad+y9cS2-~sv~d^n&i|21?73$@A<ymWwF?-?Vn zSB>-vN2XDC+$JhDJP4;!KhF;OY zAGhfa`{boWFIf$MjEwzm?3>BW(UR9&X6B<+kpS|>$uSrlHtk-D8Dfair)KQE!)saG zQH;qd%-z}K`d|{zyr(d1(?!jty?gRCQP#RfV&NFi{xHjWuB>0tMo-W4wIRo2yA{Ss ziv?y}R^IfL^1Lxyi|M9f)z}B)S(3seg3el*ORFD$DXPRC(kAZ=G9C3063$-KilJds zSLJA&o$L1(h>^(cxH?ttv?o*~vBVjjlU023V52QZ-pMS8p0 zFRky$B3Hk~TiJPCcU}O1@jM5r)OX}AbEswaqtV<4PL+U^1k3xBg54vLH-W$wIt4}f zXya9N1%~hZwx2FwWud@G!*`6@N*6d6ew%&urt{(Oi0IX+0{tDS;>F72>4HBXYlVP- zyp55nXB#aSCA9s=%UxRU&9SS>d#qzhX|bn)sp^cKHI=InVxypIJYd2RVi%wjpbO)O zRK}WjH_^ycoDP?D-4L1t(>XR(ziAAq=MC&1XYM=B1=5$B?`U{Y-d1SnN{(E{(t}(# z9Ld#_m-6CFdi`|9li>v9VN_59QA5T4-Fo{zjn{%J*tZXbTL%5#NdZ*yPIo3Xi@GSL z_WCyV)nuShz-u)Ym0D3B)OB65S`7BOFOvQO;uzw%^%)_?5YZ{LBnm$QqUze-DKsHG zoYNgRjrC0JZ3>G(w7@XDb;zd!0;yLGhIB7?^>Qzdr?nWu*o8<^E7{LAJR$xkZ7$v| zo1qtwS<=+XgS6ybYwU{K+Gw!g8E6P*yY^|^dOpZqRKLhTtwl0hLlDhQuSNZDv8E@nj97{$-rHoK z8(cQ}9K0?^8#e8P4`!olb&1#sfO$x7ga&#faMSdriGk70pwWu6g(xtOKYEw@$PT}c z9w!F4D&x>YdqmN08zyU#dm9Q_6P~yzuzp$>c2G;-o;TbFs{~&yiZv-bnL{e#Q9X0m zF{T_j6bYhWjo{<;nKGsvT)9bYHJ7^Fxa$p^?UO zK|W9$R2NL$J}5?tQx<J6t&-(malW!XiU`MHx07-3WiA-G`7s zw9>a=6hpSo}=ijH{1 zyg9kxD)qFZx*q-+Z`5O=v27=sV(;hv2MG(cc;nD`Yn;WQ>=qtWeirtMn)edai6xJx zJF#7wYT)ZK*?JW*` zPVAJFOn_NImq93p=-Bpn7pKtj+(RiMYaxn`V0>8JP5UU@gj}7FX##jTb!UOZ`Hlmk zI8wHQM6aqA?e%#X>G7s+;<7J;?8$vEW^Ox`Lh1GNOOetlYXcUQ-m%9Njc~%7%6n-y zeV_8h3{0mCwz|XDK4#9P1hd&yFL!hWpYl+b-$F1Szf<%ZW|#_zyzzxuvrg4?8~yZl zD@wqLBj|wyIU}Gt*~fiy?4v+sI4?xMnvM)y+(Z%pRH-_6_$0XmccbcsnC--Fbsm?P zWV%)dEl*+$X}ns$d8)Bjo>r1caMn4NpZK|CaatSh;r;y5uSj~^(r8?UjnsAWXF{7< ze)g}U97SKG#F84@zm^$=9)8a&Nf3sAB7SG~hjvuzD6Hw!=H)E`SP4|Z=h*itYH{>6 zZSe(anhBg)&mP_oA@yBi6e}aNri@N>j4Bo{uI82W154nq`ay<}ImrMv)zgiZ7HkC5 zQS#F?h1~YJrVqg*Je)i9QEL|TSyqDcZ)x82WQV_q$!tO2U{vqyn&cv!Ueh(XO)d#| zqDE@8G(J&|NIUzy`_n|n??wWfaW+F9oG3RfM}!D%mEg$og>$@cCxAw_3^sRsgvbUQ z!TPD9YRIq!PN>%+`mwx4r%GT4wjt(okpa-=ZRKVxvdvAWsiWAC@&)2~chcn~v|%<= zo<0*_YY}UL^$CGNwleE)iwliQ6hVdis;0sfTQjt39f#gzQ2+I{ZA z>CaX66L8`yqYvakBx%=T<@!`*1;QZYYW|LPs1GGJ@GcK5gN1RS@Xme~>IbI!Ks0(R z#-DcEjLX_snF`(XYhE5v zP!9ITCJEU&7fHIOla2W>CU?mDHqPQ`wHWDs+zNQ9Mo?pC2sl>bOk$7 zkk&65`}!32q-XlUP`KF%_tTk${+NUhtpw?iz;9~6}zDofysKVYM48$?61l}#-3 zH}4B-T;E()(s=$%s+jwvY0KlatP@OG{7WdpwfLs+7Ipj05|W`T!^cH;H)h<3pG2X1 z^_Kr#-3>r*pnVY^H)=>|$(htsuidqqePRTvJ1Zp~pD{|q?# z(e+7o25G3RtpzW^vpg`ob$Z`9M7qL}W5Ld)D^R5ve)J&&U5};DoFnNdwg|HWLd+<1 zCb08e!bDm#@WF{a%N}pT6=KH2p1M7{FqEEPDsaN-Ra;~Q_Vttp>nn(|bZ+L%4m0A8#-4Llvt$Wh z!eKE#if|`Q)11|slnIt+Mq@_5MY1LoacO>?Myv2MQhb)bE%HgvCsoGo&Syfk$MQcG z=^RfuX&bUM%WLg;EH-gS_}nsHZkaF8Om1GP;*a?o6c-w1;1>OAk&sMyUDQ@LFVS%1 zJIf|hD|TsH4^LvbH)Q5;ZvWtfj)OrL+9*qRWAWJx$nj;~W=b8B)nN;!l$E|8C>Kc( zu$<7swOC1(&_J!`BwffFW8?Mc?lT5YkH^h(IOONTNY`n$^Jp7Yp`UHfV+8ilc`Cwl z*ryhYkNVXzpOia3kg2tAN5nOm(P*a`=Jk!?Z>r~=Ju6&~9f+mwX3Y|gTy7p?W9RJB zK?yHTrd&SOXt}Vw_>_O;5>N1nG9}ZCblCM^46FFf%PKdK>=qQh39PJNckuzpjYWcW zB^sL*y8rZ@U|0C?6dKn4^+BYE*vOE;OL^3vcRcx(V_!Yriy?YBZ%Ux|b0KkjD3Hug zn72Cy%(qd^e+7`mI%z1CvX_LFCnNvBNuW2}k9x@LY_(pXGJULpn=-e#-n2WoICyyg zw@-UnYVvBdPD?IBq50P#5YYNJ+p*#WgdtrM&)LX)DDww3s}lFG zee>VPy3M#IBe}C-)>1c%lTFI=@4(;v#Q$}O^!qV`yx43;qcf0w|NSd|N7I=SkmE1X zJhuOVmH#@uMl@PFTH1Tlsaa%Ck^#bnL`)2bOo7xoPxyF(do2%(Oh5|1w-{nU;E9Yz zJpzI6ch?^QY#EUHx{+h~ae%AUE%zuK1F7W>4>RdBko!!eE2YXbhHYYyAfrqhFCPM0aw(iA|tK(LBWs|IpK9yAR~wRt#3Y^S#fys9sRL6;{l?=?3 zHd6+a24w1qntnWwWxmJ7x{f2D0`TE%0UWUeDQG9tsCKW^0s)&m0LXI+u>)OKxL~n< z;~hb8WZ4$QXr-Y;oy&>}?dk$SkL#nU>Gx^D)>D0D1>ObKQYNVqKPam0q04S{6Xqi?1$0+~yjv*3Sjt!wALC-gcmpFFlStKI@gGC_-AmBIB4V zTx1IQ03{2PT-+NFVlUU+vAqx%Z^0O06EL7lfP3^9aKt&^y7P($$iB5Ql9Ouygi@e2 z%m)K2Vgzx z@0=kcL(j>Z*H8*+hAJBDlR%=`#XQB;%nRcQQ7;NooU?P6)9pDI8oEuasfy(Vg~w^a+3a74=*9Og&NkCgTOvv14RBnw!+G=tQ#Cq^`zhcu^izy{%kPq zapm^I>(wg+<5kRV$nL@)7R=Qz?_bC_Sma{;Hv6fOm!N#?r8x-eL&s;~xTP_rlJjaF z&YE9Ppv7=cf~=0j5hS#X18TLV+H9-0d@+DcJ27365@Dx=onb(BA4hVYi>suPGf@Q` z))r7_fN94#FjEL}emt11H4dV#EWxA2<{Bo>mn06kdD~y8Iv0dZjsfUkeQ5r4Z7eN= z%)Mb&hEJHf4Vg_qfxfa_eZCirbV)J6t$Qfp#aqYM-`}68isjXg)OL-%(|xo;aY)HP z<~X7!@AC>wZ)P9*G~-jHfJpMP3&7H@D!0?(v6>ih&YwF3x_#u^*Ig$;jIm2w2YtG? zzfUHqrhg8qt`6ZHcMtIKtVD%!;cy}P^U}~OH(OE2AshkgsSPwHXy2KuGtZ($=2T-S z@NWRy6fSm?-r~L_IY2DeG~vE@4VIxUNeR_wAEM+^>`qTF*aHp7!4zS|WyxB-zxVHI~ji zl%qff7eB$fIbUg=_`BT>sDbNLGiH?kh~>I9tnFhE0T}-6HgdaSH7%?CaA$iGPJN$! zvpNz_Pj(c&TY+~M>skt*%iQoOnyvMA{^H|shKFJ)#V5gq6UswiRczlaIP83jN+QBI znA>t=KYPI-Va^JX)Y4}Nj6 z?E!++9+W)(c;64^Z_MI%O&WnyD9^umkA4gpBri|73c43uYBbS<8xTX?0RK4!(TjzK zLJ%RIEBG@qd}Arz+K~m2QlAmf5eX61LO5+fY?jkp62UeD!FpMOZ4;4SvTOjY$5S!a zQ(*6)@Av9%U0~@Ng82AR3wH`>we0~vDnA_6J0-q)2q?O>sygejggdfJ_kwH!m@sxu z8lYSNYgga8xTh2w@$xJR``Hnx!J((x_FXq$Cp1T)d6;yWiIA(y`TOOd)176o163YR zX)L+739f{S@=*Pxux-F;lq&O}F@No?$b(#N(BLZL`Cz^SNJ;h41_WzZAIAq!zf(TsywFkhD>4T!!9C+pz* zZB%=wu3=T~F|Zk&8$|6pcd2Db>xO{!i8V0a$z^?&-%vRZtD`E#nQN<%BY$kYL&#(C zaURng71lsBWr@n{PUGblAeqFuCZi~gfks?S#IavYw-DIQ@AK__Du#GCVz!JXMK{3x zd`SwOpKY~lCNO}~-$~V!f7(^!iN<@?P(-6dzMiXA*OMIA$2D zkP>AEH9Op6%0NnWO7M)|587E7$^MVnaY63uzBwvi(!-rPK+@2PL}hmQ+E1JI9ih@0 zFZ79PN#dP=cxKJZU<_>$OY%8`HB*V5h+}I@bK#7T-#FmD)dlu``?|Ycx2IxZS-sm( zZ=CAl;g6#qTvewGLe9LjwS7>i9Z#^AX=-%AJgZ1U#+|AS7S8082Aqhx6X)6^Kd_oW zh{baXW;o$dPd+X7xe(p#4%L*ElHkzq!)GVnt*YxmHx7`BJlba$^%rjE%lSI`(c92O zaP9&Oa<}Sk;v{RoT}{g^VHw8gx)n6zZQ=(#?vcbUE2biTUwt>vkB26=G5>uMy0)bn z2mFBw*)A!KMck2$U-j9j#dwLe^;4gDOl)pzm`oLr!Mod6#N?eZT8h7BZcRD7erE4n zCAYg%6C7P7{xR#gkk*HYc~y%k-2Dj|q1(t6qG9`1_lw`dF$D#Y*E5p5=G`JlDYs2R z5f|^`4WCeT<$!DC#&H{$Uw=Su4=kJtNTLu1y|)6glfhnuf%=g#8Vzmd+mC<<6I9Ly zDx>!pcMP;4=s`M{rJo9B>*SbJxAspPQHdN#4QM1I#I!@bm)x< zH>s^j^e>SG$n%^0Z7~O$=U-7fpzRIZyTsHv1pO=66W+Xg7;inJi>XZ#loqjx#;3F zy^!c`SJXe)Z1?EF@M2ELaRk52yM?n+?5eO|9=*g{=sCutaF88HVAVWx8t!qPN)kmK z0pC9obUek2tq^~3>eD5#&Jus`4#7{zF6{(E)_}>40Bo27PK-YRX39MoksMg4qLRcJ z)XHm)U=b&cPsW|S;X9I>4HEs9>2xR)LWPte7^$-1VQ=~27A@v79|!N9a29kG@q??0I7$5zJgO)dttLbd`_LBXip}1 z^~!vbn#=>f286?ksUV5KoLL@{>b^#2@gbu2Q7yZg=;r!(dJd~j-h?lv?7oe`9%OCT z5_UaMa2?LdkB!4s;QX!qi_9xJgB=OdvZEHv7VCLr z>U|aICe@qh9(^g!GhDs81C2{vobYH_(Zx6*AM5P&dNz%zT~R0kRUp~T^}#zm^}=8b zJ_$5Ii&ql@T}0nh8el)1$3VFR&5ttmOWp!SP`QYgF(0nP?{p+xDp#y;~htU zLH{h_HGB1*-H5$85LU_)zUDEc8?5n0^Yn+mjr5|hJYFip^=j|Pq@97%C=VGE)&(U8owv z`JKA;&9h>1;kQwzM^Df<_2zED1t5(EJ;bIk1T8(iV0dVY!5F@$<$baDZbti=A?FGK zQWyi?v)|H+D!xXP_qCnOFF+)1zGKi(Od}hr=wZs?rK(e@x8j!O6{YN`^irKg{VQuL zm#&;llJO)SE9=o9=wflnGp0ZDG4fDboSR>h00z<<+}Mo}ZC!{}x6nmM=STi}G`ss3 z#sF2&+X{cWAb@cv77y{}#eV_}{=?r9bD#wvNW$k&Qf2QwW0n|I?HD)l{sT04u1O~Y z)-oH5$fsq4!*Y@$m0ysCWNNg5H%juj!Y^KLD^31Y^E5&tc%T2i9Z=8oHP8lm|BdSX z3hE$_#qI--)&}#{=bR9W3^}6^CWn7DmJGW4D09J?{3pY$hyNegBV0^hLPBCu>zV35 zXzss~{W$JNDbxS@h2M@npU{EJ^4@dZ?yryb&jUa@{rQOE(bK1eNGdOgAqKMb_B(g( zumGvCO19>z`p399)78~gnNnLI)$L3Y9tVxgBxjfPaCRay0tC>+?V+kf`u{Zg=k@sW z#@xHH7ZrTtTfP%w!X09ZG?c5WlQ8d1@imtLd@&Xpn)(=a4Xgd-?h+6TD~J>3?XWh0 zq=%Hkqg{!*Xb`v%9H6S;CjE@nH|Vb?GCgOd4o55 zubbZ*5caI_*vkNvLz0ppp(0Gi`YU?&H%x#$vG=i>gKr-#MA4Lx>W>3>Kyw~qdf%8d z$z~xb1K8b-D-dS<>*qSdi2w-OTO(KvxC*UE#weZL&NHM#FsoW_&Ek8S*U@l0HSItr z?WJqiC=MP=iNDZJ5|#efPh%z}Rsd>3|G_*1*;v7_xR*4R;Py7%rfDXXnd1KyMf}g* z`tzP0(nNgMeEo9%OWL<@#7TBY2So|Bi)Mdc>mwyZ|ii>XvWT ztJez_{VT&a4UHD3YE6@)$INTGf&~`d@#IqB4@<{7vZbr2oGxXktZFpl~qT|309I^Ih`> z=GtJs0fHy+ZSTTYRKeGuWgI20z76F$m<{YxWBYt_EuI`3E=on*%c%gFtpM6fZt1Td z2UPuHBvlj`7vfAIos9LzNs*XqwpK;CLWW9K-LgtrG*;Gg%gS@jKfq(J$TmWL5D zLT^t%qnN3BWb4^R$Bl~#MUQF;s?1JR%MLXxs6o_esBaJ%Nsy;icOKh#O&hmtbkeGboSGJjXApI$x6zx(9}^SS7ydT zE5-J2-Q9T(Bs`<<@iOLEfA0Tw^Z!^ae?mNIjdTWIV+&rIQa;s%*p7~l76X2RS};Cq z0;Cu4BHLS9at-BTU&&jA&5{iOtSVl_tsIvVI>+E&1e62Ndv7^@nQjoi~eCdTWb;HfS;A!tom96LoK=!uO`b^%>{L3SP_RabT zDDOddR^m?F3+CD>k|OHtrP(hZrx;qbuilgiA9l@^wyn_q@1yZk_CJUs0}>`rj_~8~ z_lFJvN$oO!39WhHxRD27IVV#3^F|&s%u$j%zWs*vp{h9Y+H9Jnyv5d&SRxC+)B+gH9wxv}04QAn9R)m1H6*tX@WACC zY=B0m5>PRbIR9}NG9TNyRz68+6d6_Tl%vT(pVMRI;+Qn)AuU+3@bxLl?j%!5PT^6# zfS?n4bzj%;|7v_Ze?F!Ryq(-SO4Pzzauv(gib z>*NkKKqp|s*zOO4SfG2#m_9B(&Nhu1r5)8ivoReZ37fEGpP1mM&9>y(O9^%T>hj$K zxX+p@@tGEyrwyVlilO^jh#ZTnaEgcRHvQ_e4^@7+5&zvkZBs` z@T#&A?F%^&fubsjSnxloQ;f}SD|k!w?SF+uo@;WH3-^@o@w{@g{$f;=kYESH0<)4S z0K_j?h4s9!YXY7PLu54Jq)j5$zzVoH#E7WNoPlBBbY4A342}wst=wL`c~q035nQnl zD!5h7=y!XvB2{G!jrVCDu64q5vq4L)U^*{Cf;I&LQ-})i{7cyv8P8bOM#hwvD??MQ zfq_B%1KanwpZu}N%v@cw8YrjTV~lH%VWR{A=8`eEa;wW@3=gL9A^)0;YH>nM_yBXc8us z>5O`L0SgfOr&SWAW{-~mFWOJC1t9!#h!?n zee8U=qSRKD_cEizC;a^~);{?U5vO?(#K!y)g1t_+()mk*r3Wi~fetwADWENX-xq9| zZMG;Zz>gMj*$fTu=9KWfa&zJpyefU_auS+VN#s_k{@bYKkE-s4B~oA$B=mV;V8BL% z4{i_VY?0*v+p(P&8Gqg**Uuk5%oo3E-hrdVw0E-J6?YX@52`7?JCb28+^!x+@$zYK z`jmA0o`r#lomasvqYI!^BfxU@mi7GLTh(=)T-YP$TgPdX8_~fv$Jyy4Pjf zp%CD;A`4juA_grrM!o3{wT=diUW~{D9;ElS^Y^(0J+GFg*UEh<^>HY|T>@IGbx{58>!Zmd1_4NyiyNTGaEd zO5|d!`()UKB-jv2j3UQGM$aliu-PUU2VpDF)_%nrAb5}++&mEwNuwFYhstADKWo^` zl0~}gJ#5x_>L{x*jZ%Y6@|PDn>wm0g>)fIv3Lw6U9^YO(TV z@OG#*^Pc)M61vemxA?JFC|nksXU}*Rl2(iDER{+vOdsCeI$JfE+-x>o-;9gs2_(>W zY(ZE0NAHY$Yov5R%>1}-N`4`8XH~gY_>a9iG)*{O^y1ljAJ?BOD$ER}FOOa1uoS9v znvFm7*1)f*0UIatvxxaRL!1S^>E(|Gm6`a?S<2Y$x&7D**qyo+bzQ_ftXAS->vIk# z9f()OPO%SMW|z7gm)@nI-ESMCOZ?x!m|O!iTA#gWJVO9KJHCh+Q+r&ZJZH#Mr<~n@ zZVGDm8u@3}a`>eL{o*XDwBLdX)H5QU3q9)!b+%4XV;f=KA_6Mb^_(z(_F@bbl&}in>6#RBX5S zWLV0vU^=>W9l~HDLnk&rE8kG$SC4#d#$EB$tZbXpH1DOF^_b zI@6X!+Q0;8X_4^}2_s0mBcMle)dz!c?AS3d2HXH#1KvHSY_J!D_g_#0*qI87z;<-w z1sr}>g0cVXKA6BunS)#kq{SDQxmE3y&EgjRZ?~vO14jOeFCgn-9x<0hlnsW)#^Y=i z8~R9PX^Q55@e5WhBmOde(7>9T15o4-w@@cAXe|NyLv^6qO&op(3QtQQ4;H-qHgE&y zyVwoJRy4;mz15?}(m}zeQS+6^lR|o*ZOMxwHn=rFs6bwY2(WNZP;+l*E#cE+gYAH` zbZquQK<7U3Sek(uwu?>4&_5SJF;p``Ac(y+n2Y%7SW*BuDEj195POk(Vb=wc?B()f zN)h^^btGRbVN*M`I|=ov+45#&fp%d5RX}23_HkopqW?By-K6>7`%zCq+R~k9aqIGU=vd511x_ufE(dxGtEWd$hx8DbUj@zQQ zPg4%NDE&$sb7R?*WTObDJ(J_WKiI@iR>REf=f0!lqbo3Q3=XsdSJiPU2h@D|3k$EA zSyNW&%lJ_85}%0H_x!TKX9mpwvk|+!*ag^F-S5c?nhH0Nzc`#a=tRALQeo zA|tiRD9M=|)tn^UOW6=YJ(OD*xi``Akr6GJ;*mVFk%%0Ri*~dC5P^{o8rd=)8=(<- zzN=Gg*3Hr50*khdP>mvG!T)W)^l`zo)IvmXfx+8IssxW&rF4D1vqg7`#M>nku#<|d zCI#rHX79@qHB>G##~_r8?s+mPBpbf9tXMf&+A4T$SY>XVGr1;(YN~5HsbJ{{y`!vJBcshNaz26Lk?ivE~&;>}@Dcb>g9xu||yY6Oylq@0#IKPse}8cdm^F3OhRq zrEtDEh|@=oH`WVb6-Yr7q*>r}p2y#(KI76g+LnBK9k^C5Pi-cT0K>FG(g!9HhuhQB zz;Htk;^KxlD2lf(8wI*Wqdy$kX87eG=h3R#ro%cOmAJ!dvbq{@SRSHtr7tZwl=xvA zGFrol$ep*v(ElWSq?^-ShplYkF4A!xJ+2yQ-G-z%q2bb%a#bFi=(_kzbxIz0Ml-1P z)%S_~vjJrA6{GeF&s>92rjwQ}?zn3dnnaotQf0v=3jZj>hwn9sV080~U^EbE7DNS# zIncb@aXjl(-i$SsPWt@i$CpIPV+7oy;wW;|vAy43^2?Eto6Cd3+d5h%9_7^xPIH^x zT?M#SANA8G&ow4W;8yb>8=`4?V=%JuriD9}{{(kEn}==;t9GRIrpDJO_2qhO(G8(R zzFj~^F9NF93a$VL-pMx~_H{2n(QUEP^F>~4erBQd!zSS4I6Bizw%WSR4u(+rzdHt2 zkOH18RGhU(Kw@XJ3SQ(<;QgY99DpO-#AccinBrUDw->r>l{HTRaDV4C^C<|AAv3rD zB44{3nn0kBtgiwCfHhjR@J{vV)~ zQUrk#7e&AwX_&E8}8jZ)T4k0vnDjO z6ILFy&e7qT(bQ0e(k&2jOtRZSg4i{xhn{ixxMDM` za3oLUE(bm0th2+pfbPL@ve!d%r+RUVbN3NNzlBNsu^|S8zq4y=Z2s}QOMmC-2_bie z46(PB=nkF7J(;PpWX_K=s5?n-Y7Nv0Jb%hl(mMqYvw3x&MevdwZ4EPT_VNb7Wk!$q zr+6;dQz{rrCSsgCBa~T+98++&Pv2aVUNMlkEiv9oRsteVh;gMGxayQ! zon=J?+~q*yz9@gJuT_};lvtdc%pbqwQyJ$Cy7cfo@jW3_- zB972AAF|FL7S*!y4u4cK0q>*=&xs&-Ws>jvp5_15b>-nuuThu@4L!P+gj~xYl_jFZ z(jrT;P4<0DNw$_5QBjO8*_(0=8h0#Zn`y>AQc09#E*d>j(I|ThvScZ$d*-IiX#V;= z-}8Jk-}8On@60*xd(L~*H(MKpv=aF3wZZp!U*eS9SHqYajNaJoI8i;mldmPLgm2%%6^~BwK!uStJHxMU@o#^?kB!B~MuJ#?@vNrcBme3@BVzLzD&qHZ z#TFaUSRzo6V@~1kB|_VR^e)!zpBZIUQIBoe(7<)`R+6doSpHpXGB=hvmOCkfsdIKY zHw48CWG!w3YvGpMXtNGe)8KAQU6`t$=h(?p#acyo!5wqhKqAk}$X!nz7?SGQwf$29D;H}l>%GL+ zP{E}xIbqObl$|f)<$d01&-#F?sTJxInG?@fqGPQ{Qv^H5iWgCrTBqjd#`r6$^`53r zZoZtg2wa)O zM!^sg%uJSZIvoom*$AYfW;yA16cd|0n?SO&rR};O2d`ax{6DS9c9^XDJ6#^o{m{fI zDxJ3Ss5Z-|{Trt*ri6D`*Ty&P=l@KH$S(UEwT&iH@|TEQ^eYwdio0q4+qaPnLGc&* z-8Fzxwhu2aFCjJ62a$v#!XK#;J00{Q5E;p@w)`y)#-D?4zg)uJ> z+=vb^|K`j8;3B0EZcOb0aQjA`j@5P;Lz*cRT2)bGF8d6zbXa1na@(3Rhnz`H9~^$k z428cpOo9Fh18&kZeZ&GmX%rGdXLq{_VoMI3YE^4;>7_q=0rJE{V&_eyyN`p|M6dx# zy*LjUsE)fomEtC*B|aE<)P8{={~3dVXQAD|?d{FgBO?hr6+M1Gd!_gDG>R z+7=|cicH5BLW4o&hxwrst0=&K_=&lQ!{xQqs^O6kDNddqhKEffekQ0R08JK#1YM0aR#^O$28d3kx& z8NvKMQy#%}A1gN^d{8CcmbmfP!i)_w1u2?p{SK~#-lb1jjt`f6)p`?}nZrDS^bXj3I%mQ9_gufWKf%E&V2`^K3=HadZ@QEh`MKB ziCu!jm3wKVV>xYw$S~_RMk1k)c*SFFO7i4>;T^zSwQ4GWwd&abHEexn^cXouUCWV)v4n1LH`W$~#HgpfrC zV7mqhP&Y)=^=-}c9`ZBfVG!@IX%|K^_3hJrwN&+YZ|t*{AoYf!8!*HEVq@)B8p@`< z13b4D%U6*@nSVGZZO@29&G!pkY^+d&t!B#C(52fbr7AWm*46!tdh`${o*1jYo#)a6 z&yJd~(6j_q5<=IvQAj_rPeBwHuf*J{)!=cXCLyRS2eyKk*UN}8Lomg6T*1F9#B zyULE*Afu~ll2twG1op`Yflcf0M5TT9kIr$6;rLTv+b94j@NA_E2a6UmNE~H%is0oO zgJl0%vylT)u@XdT6#2@NFrZ&`0{SPM+e(S!T*tzQ)d;_0OmmE&)F6f-wtBV0oYW)1 z?^w#>2Yvgsk)H{8yjNU!k6I`M>JkXJNlV@t_F%R*GP^cOW=ChHzOXAA9o_%{ literal 87299 zcmeEuXIN8d*RC^T!#ZL?L0~LMhZ&_vS5T0S^o~derS}l9AgBnaG^KY!Xi`H*MWqR$ z2M9%^mk?Tr5CUgy9N+Idzt44keCIm;@Pd$JKl^#sy4St#wbp*7p{8*70P}%eyLKH` zymdo!*RI_uyLSEYc>i8_#d0@eM`sMJmR9oj~O3u$%Qb|gy@}EsaZbo0S zuzvj_BVvjsH+8mZrmOug1><4$o*ZjO>P#`Eqk{fJENNxiuXgWmaC!LWxDi0A`Qzu$ z-t^nDKVN?D5vlR>HD^|i6F+}4R$$ok^C!ap18*xeVko_Dlo)jA4CAZrZu3}4pXbqn zW+@SzAw+t5jbmGq4EvH-N2Gau=jx)lY8Cs@om=-_FEL^e_E|o0Ca8mFX=!Po(!;1K zshaS4VF+bNHxbgkeY=05;pcXmN*7o%Se+5JkHqG%m(djcdVXyiuBMYC()={DwBL=Y zq)&RkcG#JytRs7vB|39`Wr{f2N;5uwNkwC%7Jc&g@#AGb`resaR3$a)z13nk$T#89)8Bn<1yTd3bon-B`ulHPAH$L+X<`&Dnvm z!VbDsWTv0@gRZ{J5ov`_bYfMIBN1_R$a|hMBF4IFS2-z)Hw7#uBRxIP&$Br)+dQTq zFRv?H#`Lzic}j2Yx)QH$Zg(_)g}D2)^QYr+db+yVsi~d1dU{TEUQ2hMEiacjP-c6Y zTZabbF}V(ee7mVO6T`B56|Qc$+3}{kQBhI24P(<9U)3j1_Lcb4_wpN+857GUvN4!j zQBgX_(8H~aSmCL>+F_>Ex9@#2XJ==1w6)XYkQC-su$uHa9gftk1tZCnp=7gTZtUR;-+L8ZEwY^QK+n%X5|P zGp>Ck9i1Vs_~6;u9*0>X&Z!Zl_PKXytf41|fO;MlcBD#>-EZ-sTGE=l`;pl>^gu1T z%6DzPf4tDj^2Pn-K0j%y5~ep1=TtYuvOFBctLHJ@ZtgMP_s=v%z@$>#kv8A&#w_LQ z9x;N>QcEbY@8Oa1)*#J#{WK`k4o29D?=SId(;XLV-=b-2q6R;%8r1ne9$SO6lZ#G} z38D+p!*w!_}W%5(J9^pO06IMQMym6UqZ)h3qtQ z+Em@$O9YI|+cQU`b}r&Q8O4_ZpFX+V|LTIm06CZ?gYo$BG{npPHka)q_DQm-H&#ol z-3&aQ6Yu@woxF6?cJ$v_6TTE*b;P3D5~@lZhkPm19rZ31>;ct`uJgw^)eae@9%2@{ zH8V3aJ6Pe?N3Zgy%QuRfSu&h#@^Rxy`H0R(Q5V+MVmFMFq|rj2^ZFTCSvhjSN5Wbz zUc89O%q&w4`-yG4Q4>m-*jU5Q4GqD5p2OAni521L##d?Q$Z&GkVZ)9X=v}}5&?3co z@mosrL@dww*RMBn-o4X+t$?@oZO^O-+pf)vooi7~k{((r&s2HKTr{s|mUq}N6N71V zVV`VEVA6~?sq$1UEG#_Na)c%NRMGt`MjinHHCR2r>*Uvd5iuv#D8;vLyWE#83#G)$ zg_E?btfIz9G2p80pF=pV({zlCvMSuB#PS2SHrMaF9@^R2zxsIfWLw|-Q(yS!yGtW& zU*k_L4>v`8hO^g}&o!;NWc$|n=8d~|?>08Ry_O~)dSLx)4azF4rMIUiIFc*$;Ds#8 z6W&trXp>6!{=zqQ#?e=!H1Y~P=E`6_qe_@32bqHCDr#+nuq3FF?%j(! z`xDUiuPH@pcz966eTwvJd?~JcyWzy}rfXl{78}<1hVpZAW;M~Tdd_<{ZZMBLKb8NK zOHk0sbtp|C{K&eBzrTOq1a)DEse}ynS2mK9mv@jm5|E>P-p91u*}T}QBSau2R>Db* zLt9o>HX_NzS955#Cns)G1$O59_wTaOUYkFbrlgUuxog*-^BFuT@M?Fljk-}do77q5 zN7ruOzAYZG<(ETED=87OOh`hn8MI#pn{EhWjWeNs|Ew%4%>;G`Fo*L#5!#jK4O?X2 zGnJ`EH)_B9AHzO##v*R>Mc059*PtS1}Xy%E8*GVt%*JR=-8 zFR${-Bs!m4E7N{;x`X+$<@6isNy%udBlY8xgR0Rsf9yTf4NGvJ?_09wENR2z zj~XbzMhICpzPxmpSqRgQ_TotaT*>|%5>wI$FhbmMvpIs({whBM1E0p_P^X-OHZ9SE zmkt*6vi&-d=4g$7{`rT8Px4PsdW?Wc?>lq5Ft_u*^WrMK$P2zZm+H+U8rJ$f0nBE0F*xzV5|^8s`;uy5VbKGJ za4HV4sJ*skmLsz3emH%D5@O4aj~8QEHjw|xY1-FJv5Y)}T}$V6^z_JAzjH}Tn;PL9 zyGk7hveIs(fl?+-**!eGyo5xl5uPhox(as}yDtp(Z>g9Luf3$kp*D=tO(~4n>S~Ic z?8LYq>vI3(WMFFG?}#-fPnhN~HrqKSgGFE~H_ZF+<5Oh6Q4Vbjoa0b$U!etS2xi|= zEV5tI9Tw-vhuNi5Rm--qF&fflk1f{qfi=vRUA}TfPFfV)ooQ`o=Xe)0z4MeX5blCI zWF2BRzfO0gnQ*7DNn(!p_zrfYHNxlMjGSob@O&G$d8S@P-27c78XlZP}g z01oBoy`+YjuHj@W(_ls(?(Bd@n`nNc;c=Vh7TyyqEG)|v?}>kLry%#R%a)};hoRK2 zEAx^3TZdE-QUV8xI0!Bm5#Tp+ua#m`|1a<`jM4}`8o*xEhQV;1WJ)?7+Y0m3m4N!~MQR+~ zi=!6+J6%G&YitI_t4lKjvDnIjH*em&#DD(uDR5rTgCeIIC(^n!$Y)qm{$J1!8ybAn z;cUqM`oAOJyurM9QHZ!Jz|gmH6{8r95GFwv;{d=DA=KJgD;paJuyrLxMHgcM=STNK zx=4?s;e_o0hnR)!wfijYZCXku;VS~|^WI;czZs33W-Z;*sgFK8TvMf~sVU^Wc;}{q zLaCWz|og z2ZI-4Qd2LFi<|t#{!K;KBPP$T8-p5j-d=E{4ted){~8 zd)2-j!zJm<*4&cWbXEicpk+S#+fXe!Ylxban`;dAR^Tz$%PZ=YbL8w*Oq{4wsV|zk z5H$nrb7E@B0VpFv=fLQgFB`1KVJbILfXF1*ps04lGRcWa#~yGq_pvXno+F|=mm0q4 zbmQMF=)u~}|CjQiU%AQup7||4=N6F87#z&YcbfY6Bm}5Dwz6`=Q)V*LxYFHVeSIB^ z!Q7*weRO)dyJr_i$#637hNyc4V`ghuv};hRRb=FdOpiutyyvz4{^{A}L-iCAc%*)i z`83cxlKfQV>y zI!G;%@5n~H`RQ1o;y|7}76uI-?>p*9wXjoHBTNF)_0*uePM%`3jX~Sbcy)J|J}8^; zWYCEcHa!|nwxf>i-nHv}$Pz5aq1TV2{_oX*ZK_2T`Jky2L~dpgzSbxY1w&c5cMP!A zyy3Q-9Y^HhnDV)7&OiKPUnvT=ZKj4#k|_XhY?^Y=u$%F? zKTx7*=C}03lRRvJx6(2rYx(=v%7?RE0Dz2&nke6bm~97s0(qNH<0hA0fhqC^K*6&` zGJq0#@QdS6<)&M`YmLK2CM4tmrA1QB9s=zw&Y|_0 zg^7e}tDv@_uStJ@KeDzXY?A2^nMu^Pc(I)7YSQ!P&ou%5BV%wTVBP%UZPx&c%Wzf+ z#7QL#i>M>ApTJ1+0edGV?(3Njdk$6U^`<&-7&CZKW`&X0fQE*I&3yYH2j`*RUZ$6; zf2JtWIhL2raikV@wZxY)n@k{L1AP+;T2a(yMKc0@^*0k13gYscC(dBSm^Y}8&_l+{ zvs;xj*bUc5_G$|UtBi*d<|^wZ4(jl;=IxWv#VlAaj^ud|{M+fR2erMXD(#h@>y%n! zd6+^Y3(D5|%LGs!0rfUmZgn9+kd2-X=hguUP)Dy%#4)hm%Iqncl-0nzIAhxK) z3MmGe+tN189X<+k0R`ha0V2`!(9j+*u}9Ko`rdz8ie()W9<7xNZ?Ir` z-j%6(&_{-c2Ya)@?(JnO`$ktl5n<2yzDvrOM~{AQm>c3jov+c2SHc`*65s-!i|}{u z^^}_pkuP5+0|F|ks~`Yf|HAS_aJPNzv0Y`m2zZF0S)?0OFxCRw|s9po|v~$Gp zXU`b8+uhu7LAg~H4Pk~=o(YF8te$H661x$GAZnr%YA^kd_rT$M2sy6)vCUhtfAXb; zV5#^0%8Xm(qSVncmKDoOWK8Y6b^AthR>1VT+P3Q#p0nLE!xq(D!nlqqTyN>o)A@m; z;G|RLP7{SOKZ=wv%~oepXu=s@8Yw-7!k_n?xDAl&^wg6(W%BEK6>Qd*P!=745+Gqf z%}rq^e=HtbUf;wqo(W1>US3Yd8_IZ=IZ?M|1|m4s3j8Pk$0{?-n)nXZfX7F zNEo5OOokS00z3#H=DvD&+^c^?EQz26l$a?)a@rBj=WwETbA81FP6a+{4-N=ch;Rz2 z1LqgLL1qHhWNc+Mp;`-GX%}M)4%`hHF%N%@^lUwIb&504y^Zn~|pDHC8m+;)u1UXa|g5S(jR$ zrKh3tWHH~#P1qXO>y!yE%3ofu0?@zV+6*|f`^}DPSJ(>XA_qke#4fsA>&9)IdHt8<2_@x%mcI{C zNJN4^zcG?MT_W|b+;8;vm1G0z&OKcU8UWZNLPR$=H(#<3sO6&1h&mcKqI$zC0RZ;0 z;vub-jXq2@RZ|Na+y7@$!3Q>B3p&QZp8m?#j$vZSd!F2Umwu;Aeo~)SXQ8loG`=^mm!he4V@?QJ2bk0*JNpZ-7 zE>{VKu0|_!PT`XXps+rYpQ*9ui3wYZ;cf8TSF^R)?m0Aa)u=W$7+W(>iIFYd_r%}- z!~X55kh%ywA$is$$31{l^78hrlSv%d`2q%P)hN5Lb(F@*;ztAnR8w8vavl25qC9Mm z7X3oWT5c4c3R#4}em1n8o6tyI*bF^ZaxFL*m-D?i{b{7lTB&kztkx6KRt-e2WKyXz z14J+2WRbDrDxW`RosG%P$S|LoxFT2*2u+S=7e{&Jj5FTG*!|^sc&U) zfDy|14SIeJuoGB@6u}fV^YifZTW|}uT(-? z*dT28?89v+u-=a+m2Yh{U+yto!`e4-Zv8$LPMpbQoc&PtIgIH04sB3ObBxmH<;uNg zb;d+ndS>X7%r-c5vozgon=8vYG91+)kb}95NUL;jomXe%vSF-ts_u<7w^9!n^_Y`0Zm{8d~ZNyzJXy&6n2hMXYF<>6qh# z5yD5>LL+RBm9*wA2`5R&Ur&;^Y2CIw{Q);(Qto^k0{BF86a?@L2w%qS;)%FGYymYJ zWP;0wwn=;-Qffe^ua+R5=TF}di`Hm~=EqTSOS_3`x$G-Bx_L_Trr_Tg$weel~KMkcbQ(RMBE7Dnk2QzRG$bK6)Mca>b~fC>{PV6 z_)P@3HtTH$ozlCx+iSIBZR+BOSI;Y9Hj3hS;udWVaP8XlK=fKww8lfkM&GKTlR`kADF*obRsXT?jeYMuDwAYD=`S1xS=qY7nWT>(cA)mV-kZY(V z+Zrou1QC{gx=i2AK{&CorgS7i_)*@{(M>>Ss8eDYn_~}hc6PpxjcJMf>H~GWuqmVx zcP>c^J>b9u6!tL^PBia{i~0Say-SM#p3@82$741*@~WIn=h!_*cSR@z=~;5E|`32Q*6BExlH1Ff-!@YYUG0Uk8Zu_?8$NIo}pFz z&?F?hUgT4Ipa}h*W&BADQh=BpqM;iUim0=cM1TZRmKzYY1~--<^O0CW`J$a7LsZ0y zNBdEJI8+I*h1ceRYR~V5$}{k#fXCkU^Q(pmiMFX}eh3qok9>Qzj@r*$<@?x}+~oKE zcjjrZA9mZf98?*sqW1OP_#8bu8@$FPI9I)MFy)St$ErPYEDKC)##c&n0|%h=XW^q^ zM(+Cb>^M?11QAORE)A7Th`{`YCAYxbjkNXNpYW6T94mWOSx9MK393#LBmI5ZdmS@Hr|ZvC9u3z$4dt39$h*j zqj+C$=eXGgW2&HNhFAM#kBpf4tQKL&Euw~&m<%kcunS+d_mJmo6t`|`K8c%HsW6j` zQk_@)4f+Na_u9q0(x?v(f2ZJk;=~CIXusc%jeVGi6t;8u#uayspF1VDLmLt?Oa7@m`X|G3=M<^slx&N()*+xW-4$*&;Lmno zn9QP%@vvhE-$g`WfWvtYj!T#9TVo4K?tWH6xsHQ6V(-bSTXF1%Mb9&xYKXXY-I&`@ zB`)Nw3JVK!+|g#aF4S*ZmVJ1HWzmzM$Uy~1%A6RWq;-Ol|E`UGH zwx>L%rwxmLDKEbYnWb%i(S%{CeH8tI%$1e?3!iha3&e?*bj66sq@)7ST6v_Ur7hX> z7?LF*eSje40ICThLc_B0iBha};XuvzN^?O8cLf3HgA`KqCvG03-grCcP!+gZJ0>k8q4^2_GbMR9TQ>6y2q)eMBm zl}HT=MVfZ10~QL^Q6lZWlo%PR&$Rn<(T z7g%)!TH3*ElB5y@?nFirIkeYTk9q`j#d~gtL1bUNTcXTB`F6dO-p!qJ2e@``yjXyj zU+3Vr5061}b9Hr<9gc(axsQDuisFZPQv9U1FOK$l=5?rZs-NN&5NI2YfrH>TtCa;E zy;vDD)>2kQ6|IJ@ZWO>r^VhG3#KQsu z17G?)`IuMFvzG;LK9-)GyhEp|h(#QFHK;nLm_xl632GdXlpF8gvvQ!d8=>rboIq~=U= z{mWTMDV^|K3&edX-aVaNT}JLIpx_-bk_FR{0#XM0*cMkxuTWJkd#_xhMB%qYvu7BwsABb?u+w3(?M=vYARm81FYQ_*$@@e<^d zruMXz!sd zN2DG(>@Ikp!2`^n=6No>;dsYU^;hMZ=~`xD=W!c*(}`@R~Lt@Ty#j;NOlO?p2ix&3Ck0_ z=lvzc4M7O5y1sIyW)`7<{6?-sw9MkNxa&9uq-UQ|BHE;&6}SjMG`FzFn@W$l%-{al zYrin)b8N)^blcpT*Kq_8GEsKQz>biA)zVxkKY#iruJrF~5F8B{6R>r)gr62Zi;SEjsOWI?zmRPELjG zb$oznpR{qc_w5mX8tEX>6vYWIGYOjBBM;eLRtP)W0j*3I6=mqF5lGQyyeYzSm^UC% z8jbDj?2NGv_}6mARaN3*G*%{CBkW>Za6X=t9#qoa%|tL)NdPqEfOQ4|g(4FbnWN*Zp## z-C3&2m@M@q9&T>MOgo+n7cPv`fxtHksfIEOE5Ww+yyj4jeEs_F_SI@Mt?VMVprAUe z;meCNhL!HwldW-CfUxlHjLgj07&Cvn4p-f>d;ND~q-C~#q|rBm4zrYaFWu!gt=5Nb z0N7#XE7mXQv6Qiq0n>6u9uAJ1c6N5>3JO5Le!Tba>v6$AARSh*nL$Tbv5=ta<+Vr=ymBk63p9 zIysp45SxsDF4W7a-ZSW}8Q1z5!0N$F`3=>U@inlf-duyU7cUM$+-a5J3D2)= zs!{Rr_ngR!qxGIk(a;hzK5of+)%lvt_GU3KRZ6Sqh%NNHfT4C5S+!hLgyLu?^j{($ z8WHe;Hdj>Kt4NJ~N7+i0OS`+Zp)+?tKrME_)5WDw{>ABYT|!Z>U#D8NM9;uR928ey znZoNSDLqeLBxrvt3Z?RQy7?}CyI!Ty+SsTeC->OZ&5cCl0>H=yze^?wdkm@-mg#6} z-VF%2ApfFWbj0s?NxY6MKN!mJ795baiOGGz2!IC2qd?P0U>34{Ngpy;5qi=Bo+#ur zEJ?d^K_LvGfEU-Ne2N_PSpUBB0Pf3U?91T>%I;_l#3A5!lGvL|>WR6~v}p&9yhi!a z#F-QrUk9JbP9}D^%JMOAHt& zF0l0M5f6crUc3;)cY=jP`J&)L{q!5fvtU(7Qo zmd~^U)f_bdjg5WmDfdle4K5^Y0l*Hl2qi}F7%z;}6{M$ML~>d%gV=v8f~1Pl%F1+K z3wbRVSKR^8C=)GV4i=Xv>66;oshOu=2zJT?Jd#&0zq=<#ucy8J&d5h0+xEX&6Qzt0 zYz8JrS!!S>;`R#Yzk4rm7(u`6b;f4qV=-`cz?%m$J~l`3a&XU=<&pXM`Qg6rcS2$g zmRM9&^r%hQ)wSr9pebMW1UN{Wou=k%03aK3olbLWoT&PlGiN-_Ei)>{Je9stMXkS{ z=0`4qK5+0LX(E@dsHWBn4U#0I_Q|hrEd;N)PixRT`@VivK?$UT2Q+kQEpPWLD=T-k zr^r!AxDhO*Y3u=;g%A-aD|nfl%*-!rK4rcX`-H15A8VsoINt>#J)rGA>hPKO5C0H` z3nJ!>h>)-_Xkl^;7_Q8Ri={zdDl{w{ZcVIv|ES1?K}TO7A_P><-X`M*%U$GP(*y+t z=Y1LeB{C&w=uay0Uo^PjoLixv%ZX7OJ@^>xMHnS93`I;K=)$vhYH$M|Wn$88LKa7K zQxsMM&;WY}_>|M%qjbmm>l^bJ7xhHRS9YA<1v~+3VM@=Z~R#F?mo{>~Gmz ztUv0>JrSAYzo7^fc3r4k&AYYLukoDkLOy!+i(jbaYOxR+dW5 zoxK{B33cic1*$>ApZdlIXa2bx2uD0yPJiFs-8~KMg5q8a@nHX@Zc}Qsnli4G-xV$? zbpkDdod<Wxpq;@E!c3IF}&qHo7dkX1&R@8yPA=J#_ldqE={meVZB3xtghj0jkrF8xZRUg|F|v zawpt9>3j#zY4Tu&MV*Yx_fN^GDH*u2(zSTQ42^G_H*^`g+uQ%_aFO`Tlfpa|=TytU zW2SOtb;i4QzwF%RAy#p1Fl5EX%3LF%daJqKyfyv@y-BTKXoIIXB6Xe{LiMk>AbXBb zAnLOPZHt8QvW#}X!5%{SEc8{Bz+jb;Z41Gy0{&b0-QM3m5?{%EABHj@XJAS)yDu7PWyH6}r zh5BgBcwLRdc;l;cEx=3qiFR|0_E76Om-gVI9fbe2r#YEQTsivs!0`N}&7L-dvx^$Y zDsDZSg+6DC@YWN*5HeJNooD=U);d2aTtS|_2bEHu&w zWwH7;IcpO#bkyBeRSl*4YHD^KAExcK>X93AuR9UST%t3b)Lb>vlyl|q%FjbbT7yTe}C+rb=++ki0) zhPWti8GXo|$h$8r%>;gnSgO=z<>%Fb_I`7*6z#?@Ryk3*xqQe{SYte-nPU(l<8B-r zHN~BBahvPImlYu?-_=`t_UsA6S&OIvVofCl=t6LXSn?S2iUTnwpvrI+DHy$~I%#Qrfo+8})o*sY49i&?rhn_t>~QOK*u?7hu19D02pw zfZg^MRI7c8RJsCMl7(HHV~=u*i5Wmb)7;k9n|%E-(y$E3aO-()AUM%A*b`7mwG9l; zl4PMuofIT->i=9vQk^~O0>ylM?}M>7(P7zeAO|X8OG@gGsPT`}MKS5W0_3f8{~t5mN#wB2L3+wQmW9>1ZHa z7*fJeOdz!FqHb_#yS31_Xk;ZNr2`WCx(e#H!19oYPZOTAyBn%7exs=7y<$Pg^XHG6 zgWgN}Z|XyCP{!F>rV=N@Kiy>iy6)mFW#!@+MP=m(!d&S{lBknuA0yAALI6}V>FMQ8 zwd?Xgt+o1mTwugu{`@DNd%rt9ggh)BHnk~&lS9$Wf89!B^e-d;mQ9@k)P;ar((>Aw zxCLD0kp6HpHCQ32s!XcmHH#|{-p3|*+4^vu3zz(5n4$i+D=&umCZZf%T#;|Oe zMy?~eWddxfVZfBQ!D?WW>H_M}bl9|h7ZadihVSk?OmAnj|Mc|O^`m=Vrf+QczMU*4 zdT<03+DPTTpV<^XnZ-Cu9SI=Bi@80TJQFAGVSgjz)W1RbWtnQ66l?fy8_1)MNxhnf z?j_YQfx=~Qbl&N*5540u+w0*7{YXBak*%K=oyb{QT53KecOYhab4muApMPXyP-bf> z{HY^@gW|yV_#ox-u`N1vYyv&kj{5MPVaY+eu(ANnWooJOY)Sjg`7v-QxSVEKDH``a3hA1j9OM%p3u9J7# z$jAv^!**2k&ZzY_t5w5j4S?<*NY6pY+7E+6#sTXsW&`vaby#bBB7XrP2Uy{hAs+H0 zfL>u3IBB4ceg9tKI-%rKV@d?*-!LYs0NO%+KpdA;iNAU~_$a$Bq~{|I+BMQwx>qWc|e{^sx~cv{4Wt@ zVirIzt)(qUfVT6bS`?CQ_%?*8Lw2NNZ$Il69%NW#3FE-a1FL?JVvD2)7R6t6dGu4y zfAF_y3nPyP(sT_i4l?u^byWdsjA_E^M7-%Nb&J@%y(}oc=^>M`y3J0b2mZKf)#|Uu zQs=E+(K4*pJMTsD$Eyvz`zFdV=hwKw^f2}@cG+{Fm{D1%MX|CL?WwseUKgGkw{J2S5-`7Ot|f zlC+%*U7b82)Ioj(-&sd;ggMfIBC8NMvToo}$!I%O2E<(QOsaHYHd_Zc`3>4Jm@J3n z8yO0K*@ zQap-1q4kg5r>5`pB%Vpd4KFjvMBQxi)qaglmDC~}Ii%gc^np3*`?gB`ddkL(Y%}}z z&~}nj%|u7vs0jsgQO7InNN*H*@APd&?4^M3fd8{Qjz7#leG`u0r(PUUN)C~^Y zO>lI-Gp`?~_<}@%kwUFC!m=PsXkBOk5m?*U7{(y^jK2AX;5cVt(=G>j*-`6m?BM0o zPYy;%2F?ro0~wqisjC~~zw_(X`sU72wiUGiNkVg2AyxrSnk2kKJgIo;#xr zjlejWx$dwVQdfm_|3-(}VLSH4VzP3}w@-V$7I%a{h=LN>1S!fiMZ zTVr2BGX0$rrrwMi0n;{tueNg(II@}BMWyHcy(QTu1W;zkcaiBhxv;!!a44WT$PvJZ zPBYvV*yyr%Vw465D$xyNf-0aSKm`+`2NM%<#TgA-zp*h_K=SzV`Exdeb`YY9%vi2E zC&QV6h$pvZzA#v^>|3*zKFN+=eFd*d`ND3^It9?;YS>|*@HKpCLG9Lt18J~g2cE#0 zdLT{rk;FH%JB;@L3oi8}8PYr)*s_rZfr@NHLs?Q^p&WFgX@b5;0k6v`FYgnG{YVOi z`-5Rh_wSz=0G0<`jE1UePr8zJHh%Fk>b^3UGJGN`F>!c)d>k5QS6BmXcfgG!MjkNJ zqd4Ws(QgIiSwqHe%_B4=o>W zij|qb?`%?lFRczH99Eaw+mJuq<~I4b0E>D>jk}=}zn|X)`fwv?$J&Tabx4>1>2D4~ z%Wc3O(B3ANezd#>(Hmuu!-d2l=uS$9-jqHvvN2U&qFNPZa6Bfh`|KQ~UYD_8mE^;pyqwzEzSQ zI8f!~)c7ur^^uaCt2{f zt{jp=u6mhBPSniW@m|EbtcX6r60bMl;fUYRlv4CstuXS>c9Cy__ZwzqMe(=x3%d)b^xR2(1;0gwOq6j#5Tmz zvbuuV{cXczkw5?@?Ib7x^UTSN_Ao+vootl5%;}uS72dEdRP&zmH$$2*$hD}q(a{=^ z5D|bKAXZ4z&r9VV^xq9)9|oc&csyk^xHa5#XM4U`QGg3TDUJa1V*y|mh&BRglpYjW zU`Sqzqq}mQJOcm_twPS$lGt=x(oSOQyvI;FdT^WM#&W7U3k;;hwYMr(CL?&*wUD|VWR()jo8*ZrBRzoKp{&C_0x9wt?j2#db(qmjjgwB)k@Ke=Wh4`Aa3r5P#qL2x zDIo|4p=uO7(+NkJSaEhB$plCl3Lp$YN8mPoxJE`n=h6@MZ8Oc5Xqj@|3XjeN-UOkF zk;a6;iwW@;hb|rq-TV6i#$&R(U&TGR_7}!DGW4&f$$!QCsdDuX`|v9x2CQFLZ6dQ> zs*~5sg{!pM=HW>;#C_sk7gd8N>Dj(IZ>7;b%khpaTi%JB3|K7owUq(Xohl%nVnNXb zC5|*wXBKVM&tg-z(k1~9Xvu-GB3rXPLb^WRJ*$L;gwheg?(ai+hNVi;R-pJOL#k@; z=%@?*_voH(AUxfjovFy7KOtChL=xtrtPJd(ob=q>ikVqhtWJ-`$bhE9eD-WNObYc3 z4A=`wRQtFs4-cEN%J{>ui51dbgxdV}ExF8TB)_QWGKxLB-HdKjY#l1e77i-Rd`|2a zmf-qPU~Ivw;}Pm`4@w}Cr1P~DH9(W|2JYnCM$}#@%gmc}v-3M>qH&dKsi7!5;G|VW!_@kb|B9}&BGG9Wt2DT1~&1Jv<_LP<)u7F6mq4GrJjVlbhc=o}svfPTH6o{W@~bBB)}b+~iq z8r)CNF+Oj|Ebhvf!LT$)UKZQ&z?p&f_T!lby6jVVUxL~LbC+VsN4&lJ{8er_ITg_T zE=<`tJGVbpfui8m&xecmi|IpoQdcDscX$Wjt_##CRAiS|q zR1MR6?}V*#`+j*Rt$FbFytOi>s;5yZ#X9vBN_J>|QYz(7G4|@)8QbD|SNJo29=g}> z9NmW^COijFVy)W1tjD%WKS0Kg@?kxnby3FdW6s3R4ZX4Z)dTaV%dclPJ-zxXOS*CP z*VCKM|EGQC*std}J$U-7k>=W`&+a=<6@m+ZkbMK4`ZIxBu=)o&dv$c0!1AzIp4sDv zcdqFy7}C!OMQJ(dBz09))nAvreJX+@vVK$pJO!Sk08d%?b)i44{xOhkO_DJS4Gjf~ zoAqz|&YR!v<_E@Ju?h2&PJ&u_0hITJMMQpT>5Vh)^5~EMuHq(qyr;K!AT}-zCPSh9 z&JYy4|Ndt^+0aj}k~?dAF#X?d%J0{#_J4cv*P!!X-~W#`F7{FEvNpd^S82P7o~gBL zcf;et3eB_j4!6QLt~y4+E+{G@gRg1Q(XYVmp~S6YX2$M`g2q%d3IM0OfdN_WoAtXz zf$(4$U3iMcn@5ixCDHx%iJY8S`5?Y$GgCw+ilsCHp6B!%K-$Pw|I|uT5ue!EFihR6 zOnaA>vw3LA%T~%>Hqo^kPo~Ne>x=jdq7lEUq4XL7VujE9@tNk1j+)avykLOW2(Jh9 z5t_GETl(M$H91#o+NL4-E8^pw*SE3B0WLCh64uysKh8ho|lY3t4V_w)E# zI$??z9<*=>nlG^F>EmUISF}~srmo77iwnajYipW6AJ36_(OQ`+d}=D{7_Y=-566It zAQ~^F)9s0C#vqsX7v2<}$xAs-CJ#kCChSLoWceN6TCf|HUR8D?@pl1)`TEGcdd)ay zb^1ewMO>w;u=6`(o3$vrmgZ{31`z{>QmB5w>eSI_G$Mi_!;@-ZNELl;&9i26xdqI% z>!4tDG7OgoO5q`^$_-@^g@=H+DXm1tyiKkrhENt#4)CLG!{`zublEj^sc5SaCk3zC zQ>Eq5tg0C?Mv-@VHY_8ep53&hq#AY<)wa;aXX2e9TOch%FkRD+Pf91{tmv3V=E8S* zSesQaW37qdib^jCnfb=ZHwIK|u;|K$ic*gk@+q5`m_Rci{9nYU3ZWfg1foh-PfsJa`1`Bo>F$gK?%c5)j?a_wTRutpvlPkKnmjrO*uV zpzJ;|6S{ErPDu%>t4mQLfdrOXwVa|*DE;`iP|dG{5rNJ~{0kR`d?9v7Yv&|~p zo!wsj?yr@USkUuXE^1@!iD6G<)<8o8PfF}$)hY=^*_c$S8TK#K4q(Q1%}G2*PS6Ho z(8|7+o;rU0zQE@%ct)uI2hyj56mKC}Kt}RK90zSfI!;xCOf_mcPx)q{eG|x9t$}O$Rjoy=7-$W;Jl(itj{_gJLM~->z3j4^_Oh`*u^0!wkqF;BAdqq?miA0PIs@FV zX>4hFzQ5Q!V+4YVl|ghEn-unseMcIL_$r1Rk$x0!Z+)0M>Op#RfPlh*!W035i$#1j z^fe;VGXqRP`R#`f*I{ZF{nh0xq>3=Usu>Va2XPt3=Q-e60?_+|ni+m}oDuiAeS*LZ%ff?uaVOXC_0)DOb^wcrqZpn+@DU$-dflg!!BwG%) zwzrJR?!9$Ci-5v|J^*>7tN;zRkb3|{h0}&+5Xn&sW9}mWT55ILA`9p*JSqO@B11Fd z<-R=)?E%`5()4Dnz>W-bM@e9JomnX8)~j2@)Qh@|x)sCS+rmvhe*73%RY~1fP7V$L zI?_-Tp_U9PJ519GR0Sdm5)A8O9zOgH=_-h};)a%nXsa0z#*}W|f;DIJwP-3RGz5qNyg@cN#xGq0BdWF3;u|EKJLDkY@^%S%f8qC zDlqBiAt4G;0?}4> zJy@ONVPlqNGjH^SpZMeUu*IN>jlbLGN~IidV{GuNO^{mmWQd}5Emtq6TEexTuSB?S z-KnT8cehv{YUSv@;U;serKsdSQRHo=o!R=hXBEkn6L?3^F;iJ!pG>CS9}?@}X)Oa( z33yZpW}QSvo)S~(Jo@pCp6rvhxmM*3M9P6jg>(p{$l}eQF7G+Av=(?EN=7us5)J}7 z%g1WHga|5>^Z3@0^BVpByuqX0G(Nc56+6+#qYiu;?Wj?pXe(bEmQ zDS)z;2GP9*zy^@^i^j1f#Q0&t4z>`Q;fs_pFwy}S1{mKw>fCoJ4&UBcG5B>i%3!c{ zkNPx~;fE?KYvI?$Z;EO-x!N-HK5 zdKPJggVmhcU?mWbAMH6<0?z_^M^Ay6Ac9qKsT%Ty0+h$n_nQbp!lPM0D;D?PEC-8{ zZ(RbzyF8hwk#e5f{bV$xEdZzxIxS)}APs`&xd^J}=vvfx z$=Bk)#1=r^c`6arCeSRu)ixHolgIu^1ImMp+r*p$$qdXA%95!6#ol`cRk<$RqAVjK z3Zh^lECYyuU;-s8ASiJnIg5x0f*?_{fnWrcC+G_2>0&bHJD<=!dUW^bni6Mg6BKi5r|eYiH9qGU!2DVw$WQqpj4Mn_kl-Y~J3t1kc*C1F zw{as;N70a^6^poRQ^~fxn!eU0OW_*ddCh|GGxf3 zJua`OZyRc?#Hd{AuV5$~jkSA0m|Acg!)bsc7-DC2=puvF3dWKfbE}k zD!Pzzm#VZ<_UlH`BR{BSpPT0r zeHOcRedmC+soDc+7moXLp+|i6*QH#YtAAjkQ(SXtnYG|M^F{ipk;NLTxARwX&AGxh z&&UjF&cPGPS5gRBaq!3z>hQonvtpRuA45nd2B{ z`>t*93p_>{TrzOI^(ddj#Rt@^|MrcX4F)FkJ$!h5V`GbV3$oeYuKtAj*j7Q$@Fzcy zl-GbyK}`F)2ep=an3)>@SsIdcIN#@_ovQy#$B`jab@Z;W{%yFlKTYa{xOJ z)fjNRls9-+nWA%eYrfm=(vY5l=qKriPSms78~}&BoLG`u%f@5-DY!Fj>YB71^Ze8U zS?go~n=pfjX+mMbd40QjuVKVbR>(e96ic7BjB8ISpt&)bCOTbs;z#aoBaxMlpAHQ? z629^`ilbJfRrF8szZi`XT`$Nteoz#9vXAG)Ss9j~NFX&o-(4(|qpKg)I&?Y9t?cvX zr^Z1bi17y|=tWKE*zxTXiyOBPS2jR5p|0SkFFEYs%5U7x*TFI&R2!Rhicd;X(bNnA zCx>E{!NbB?nIdvHMjr8TZ7i#A@AIgX4mob(PKgervCdw=70Bd`l3MIq{rQK7fn4E; zUkM6-VZjNd<)6DP`)Z`}FYGxK+HTP9=ct!3{^04P59WGW@`O zw}5M^7y&z?E=KAmM_2DtO`Jc~<`rEGYS)w+FpK&W#Gt>_`NPBM?+BEq= zZ0er?o*R#fAHRq>99DG*95bvGD$eEm(s;Y$@U>I|VkG`!0mO(9BQ*5J$QEAuO!~KU z@rdZk9zE~2mRvr#`t7kzd2VyLUj{jq-k1Y~1A0H2yS4iG!?wPp?cX_8`+;uvo&6`c z|2G=re>k|W)AK`H(yXwW zr)18bf7&=u4>K1j(zFlPp?Ti)j!BtP=5*5<9UiQmybyL#a2@eDlOepx|=d@o=ct*(RzhfvLA4sc%Bx^CfL1}$p63C2rLI3pT_pCx@fVl{Y^%jD+uq0T164Py9V zi}zl0Mm$t}U~nS7v1e5helg#|#9ThxH6@KIezM&1KrRC*H z3JTtE&~@$Y%(fLL<n}BRTgJu4O1ZncV?V%a0Euf}i}4n0zjLp9?oqeCF*C7}mpdLO~%Lo!dP6Y@}auw5Z0+qm-#PF1SKqAY~3jx zHHt2Vym6aRQZ(>vQC>!>0oHejRpr@&i;)JQf4LTyn75*wV?yxJkSx~KYygw6^krY%i0FS&!bhT*tKBcWH7*Sc(dO zeqpQW`-iQE{YV1=-2fa+4lc<$pLc!wP(BI*sDvw=SWumO^;3O*XAbZ-<^YRoR)@v` z)Fkilq&66%>z}9DGdQ|9Ec*m^?1cp*xFVAv7N?{`3|HC9hZr;env;s{@$@5Ic<{Dk z5}u*IXioy3pthXvU${`>uqb^`*CqLA z|A^hn?W8{7Awxz*G{9YVufmeyqWoq*Qn;Z>_~jqby;M?uq|HLfsdW9|IR*`!2dD_( zJ(dxsB!(!?YbZ$bFRS#{Fg}4|`@RtU$K2sA_Cf`20_iEOBDSFR3S;XeIs;7=$OxdW zG2du^Isbd=va?mROU+H$s|9rZpe3iOfaR)@M1jUj%55s9{9HF8BGEbSJ~2JrGQH(U zhoEKGedl19Ivk+p5-T>Qfy_rX4xL6XwN{>g|2~lHWo6R5yu9a}@0QSUgo_zgM=(q) zxaM-pgd|xJ53DN!`fxMUV%h^2`d{o>w4a$ub|avGnmI>Sj0v8k_)Gn-H!=IM%+D7N zI?wAmt8cOaqvhOD%j4J7T_w>;NzW7RoG!UXL(E+|;p=HgpINv@i#Vm5{P`+~q^7Ds zmg=+CK@;s>r)B>8DK~+eKi#}oM3Vtu;XQ-(*G=ESAx$lhL&-*S)iM$F7?yuFP31ZelL4-0QP(A!ezg)WmNmYfFjv`dZ>_^yLz# zQ~R)2>XXu9*d*sFkCEx^I2bpU%I!h^An%oWs%tB~GXCC~FEhEg@swAR?n~aj5n7IL z$tBi8tRvyBi3lcJkE?#Zq^HTfDn(xTdR5+DHLehsxC-67r zyTh`&-=d%=RTe_{#bTfOsqrn0T-lrBAACCYLM1`9N$n7Lh0>#U4Qnc8dZ5){-BYm} z#|>SV(8551(ij|{L${wAL4rUO)$@Hgi0qasI}n{j`yN6De?tx6Q$PJ(K(}9UV;1*> z7c-pJy{Y;mO?V{^55$cBheRexd4tkX|L2ai+G} zxjD;1A}(*O7}X>CxG8Q6KB42Fk{sT&eu1%)mpi0IFB#sbOZw(77!CrFOqsD4biZDq zr*5B*bbGia_C(I^S=k)1Rao*}{?#9RN#OZaSol8?9&nTa@eqj_7kjUVmOc?8D?Sn} z_xGDtEIV>gK>tA~^{qSPAmqA>yh1K78jMfW8N5*ZGw0*X!4Fq9@dEt9{1I*I=w<~9 zM~lycxhvj)9|XZDQ$;W7%td@-3W{et0HsjMf;s>cMRi*X5Ugl$@ZQN%=5jqQbVIMa z>c0IdUj75^fw_=z>EA9K+EJjAv}E>Ck!5uS&K&%R$sxNu(8bq1J_{J`8xXK_@&R)x zZuh<5M=)Le!xditz~cRf0Wn)!wGlJGGXe*E?j5hp>AuxVqY4rVPPy#7ouu<>c5L-Q z4Gbc|G!2FY2L&l3D}{$duvdpW>o7T+tH`o>H zF5?YOg+=#-!&hHvir|OnFheeIHMW8`M%R;c-62OVB3O~rIdMJi@0HiSu_j*oq1&?Z z+ZcFl{t%C3J>##l_qyW`EBJAK+%|b^b;BjZWCKWc_vCbPiSq?WlHbT6@vkp&W)rXY z>5yuc6+(&POw|8U6zQI?D8b ze7GmrQHbQBYz`{X0{uQrN8I@zG-q)roXfo+kdu3!h_HiLo2EwpD>b+(~9(&+QrKj{7OaxnDg?|P_!s%pH7Qvx7?iOUbQY0#YbA;)*6uWYc3LSt?4ES z3kxHsclMi^xA0UTa#1+11oW=0jVvpxR%1inn8=F}l*lOPDs+xYfcj{9ZD?X_oVH8= z>{X^e;M9&M{GsumoUr^&<)bynFV_9Gcz{gdSq+c);LUfJp@|#uPQ3jrk)q!EfTY_ZhXSg zjZ;^aPu70aP*>Wh5uWkbH?`OKnuec1-ay?q-91^d-2A*}w`_h?KX)rSNU%&<+mD`a zsi&muZV5A|lI_{%sLrF*;!FwGGfjH$9&;^03<-o9JME=46!reNQ7XD5>)+~AI)##iN1JqSxP<2dc!rH!CYXYJaxmZuqy zKfY9Y8ihIVheUx3I#>b>HTs8#w>oOWQ~?&Cge?hjW5^0YPxBfg0`zzmfSo3iT_Mnc z0c+T1*HI6e&l(x&xtjRw&DqZ`*3#9ddH&jF(>;x)L4qQXXx{1dmh+>nV+ADUGkbxOyJ>sl50ew-GQ?lmgUCp)!vSRsPN zZOXsBQbjHyS>Xr1j~pI;7S0bHX^KipKCla5f{3lA4$C~pE8Nv+wvq&6m)|!PxGG3UoN;B>x-49Qf_fu1ZWCbAmiWUoy4B;rg#vl$ zSKg&H*3}`MnVFr9U`?2)0AgnLFYkUmVueO>MOI}bDm^?s{m@U1bN44H1`vdV83a)g z#l7Y#=T6k-4oEH#Q9$sXA3dv+9;*^Pevh>j3lKj_Z_;g>V&dekSrn&;9UDq;#@Zaj!ls(=i_UZ+3<+tM5cq< zBNK)laspmyLUYzn2qUZZQ+0Zn!NzPdem3p6aN&ES0P`eK9pn1cRCVG2-DKq`w*Hbc ze>GG1n8Nj`3=f?jJ6@ZLTRi5rv$WXCcdCh%*ezn?=$_vld0St2#AKK~*n&|>*fpGU z2{eDW4M1370!Kr-V}_js{2(@R9a%4j}%CQd5S5S6HObEnr#e>HN>7PVWp0< z942l;kLuMj*epMu88F#$x!3<|v7N()+mhl$3xhZ5G7jE5CD@_;7!i5K;~ltz?mCq_IKtWXmMC{!Wdqj9 zjBxJvUcnEod0;vvk)zIwopz(57mq9bAycuMpx$a0!Wmne`b4Pxhn1&rva42_hzPG9 zO2U>AJz@?d4f z+edhHH~IaPR-PVKJ<{@xz47H0!>$U|^5@d5v25hL+%uc~l|SRWf`qSm_tj71hzti| zH!d9B1jiE(S5jEgpwv>JncJ4ontKELd;u2)7woOeV)VdXW#wmaDuO#(1A{kxI#xe! zhYF#h+~Z3kmk3mOyN^JX$O|I6z#mxdhYvq3$rw#PeDE+F^wQF)_`&E~BP_v(#$&qe z6hxlKRc#m;j5u6Aq>QSS!qOS@_MjAleCa{XNH&Kgy%0>Rvsph?6|2Wlo2IjUWE1^s z=qG6Sv(}!JVj373$qs1s^II=-#Phwrx#kTGf7IZIqoxwJoM7kRv0*WgFGY#%dOuXU zpjK^hDrD9&`gF|6EDz6nGCz8*Y)px=cPLqNq=lBt9DdW$So2zw`hc8k8aGl@Zc8RQ z*CNM*z1{sIg$p9)K8+9E*pmO8{X`XO$4|?7j(C-P2J$Dh)5E9fd_p+lD;R4d6&kNK zeF~E%w=>ZD8Z9V`tq)kM%1i$(*gvJIHr}tL5T?1zDe8kRLpI=Y!=Q1!lX>cI&5W2Y zA`E+^ex)qcXMV{HAry!;}jhuhB^&>vSiVjNNWCoE<*K#|1G0HL|}XObF3LWQq} z+a_Gpeo*%Lg>$*LjJzCaxOGCYyqxa&%d#WVH2Dtwy^kZK1hm=;j~E2?TlFwRm;YcC zmg2U14cs7QmV9HE$MJFPvWmUVnHra0D<$*)=;Z{$ORx3u4e}@ZHa*}-E_*#)98`At zQw@n%3$%?Y97ImB<-6&X?Aj#<1g`5_lW{CcZ2f*Ue-t1JiJgU}U%zf1IvMMRa zTY-#6)Bn4$+|Fzhx-?b;*xQ1S{}F5ZZ>vp-G#y%#a&vPPMfnd1|9-R9VWL{99(C7) zgM;Kht%aB3hfC=Qg^szf7cre_q@7crK9mA`}!cKSMdgbsdLz=lS70in*YNcdmxlft0Lu3ZnVi z-1E754t=aBFYY^4JX`s=b$dM8YETwW#BTY-JlBPRh%K*-xl3OAzk4(B2kQZ&xMCsRYki$mr{T^9?0 z{oquQ5F~rApj3)W9K*SECLBH&ks>9c=CGG(2U%JBpiF~k06LEM{~)^YsB4`xl%3MGk9viD z{fAaZ9{%m$n#rpCS>jx6)!}fWOMqrA*Pl_df%3kj146UsWAKhJ3H~YENBR$~_SOE) zKoWcMUHaxK6(rE4wr6ttx|Qdm-OL^?`3+H8hjtw5;dW2@tez-Z&dU*Q1a#^)=;Tk0 zJMUWXCiKrR;M%_ABjKov0VVa$E!iRH*S9O78U*94Y+~tDr?5ilQJhK4xh_LKPv(}Z zUM+pLv|)cc|(}mC76Q{@U?Ofz&wmf6nB6R$tPon*x!dt6%rF`oAV^{l7`@ z{ux@le>j;toM@CqYhZJ8b85>t`fC2ETa;p2JtA*HY#RMZPvgom++|gX znssDVHRNMY^!xm&oKJa({L<^!7x63H0i6F)P#>R>;k#ya^6n*%`(Vi+<^a+W?ng1~ z(&}e_IKK(^ZilUwj!p);0FE5NMhOLfNrI0N~8KJ&Fe`FstKl+e|HZe1M zGWVxq;`>x)Ru(aDwpN8nPVe6(Epg@TqpL*bwOy8ZY=)Y7d#pXeHhy_q@z%&Q_eI~0ZlM*qiKi{)W~g?5-5Ca1 z4eQ6AvIc}WTtD8BY5E~~ps{%6eHSNBDK67lWY5eEn0HMtTedD6e0eFkEM&3uDw~L4 zMWyViCi}lv9y~j&g*zPGw=pa(Gx`Fb#P&b^db~7k47x@G=~W`$bFMQTW*BZ^MSTB? zK5Ai_m-k*gs@iIGqE^nDc}3*n^H6V=3>gY2mh^ShU!Vp-{N2lb5aPfKVUuA$&`wc< z`P_lEyB(NMZ#=OxjH%~pOz^LNzD`w!FX_rvqX;^V=H<7p*+IX`FK3k4@_>SSIF(~@ zz4!6>r`C#tf9}Dv&8&68w9;h3X;ydsCrkF<87oYkYy*Q|c&(~&9JCB~%iAcn$)>j` z>S%1WDDkvUR-RUIRjpQn+m|$bhCO_^HDE{2$vI*t(?^M&MAZr*;PLdcJ6%6B&{4?z z^LKw%jBjPdq*iY~T`oziZ7n{dltvv z@CP={3?4mo0%P6@DV6hGcsv?6y8bWD7xd*LU5Pc>yRs&ct1=-EwDEX%@F}@GhbxRH zs@C>iy4sokzO};Im();h{rR6-fR*k0->@LlS2hu?bvn70BMVYJAspdFbN$x~mzOrq z&2)B1F3Lz`6h-|uJhr-4=jPML=Q~`9j)i_=VtR{=)yuBXbyz@7D11D0}x{Dq)nvX`2doh)96ls zOW@I_;rtPip$^x-`}Np{Cjxx?f@&dgBWfyQR2dYFhIax3D=xR0W4i5#?7+_c^Zq0> z;z0%ojmQp|-eef`WNvXp^vRrdd(!ZPQSa3bCo3JOClT|vi17lrDJp0hK*lvfgvZY0 z27oAAK~=3n3h{z=XFK-(@nf}L)~<-(`ju$Bg#>T_pu%7uRj#F5Fj%1CZ*?U3UI%5*XIGnWwD!wilbzE z13pTzI4j306i3#^c2uLxx63kj2e2|0S8{QBIT!84nNG7l+k|OqZMI^9TsTx~n|i-L zIpu}v*@U1FA!%4N^OHw0tSY}cWM(iUTQnXzn~rWun3JpJyfn+p#6OmYe)qjP#V#X$ z#4sO#0_O1-Y7OJiw{A~FP!h^QK#B(iPMNKq+*|y(cH9UFW)a%V54t=x=y1XQ;!wT^(W|; zxI)Bna$*A0<|R?-&_6g>=epF3iO!n}4?p7yU;6DSN(_O=BfldXz*l(c%LovIgEFyg z8(Ex4#E#lbE~G9WSZ=kd>iglzQr%zQ=J5T_M(gFp>FZD(0VLf!*=^96tecD2trs_k zkaoZ>mQ__XzP*?7t%NuZAZQ4U7nJrn*Q*{ZijtUnG_hTQt2R+1Jw_0(v3e{q$VZ4L)CY}m2mF4uS#(RPf4UPvwycBITgvex3^mKjO zRMVzh#IN{vdP`%`$$Bh0VshPfuEV)okb)9*-bxi?_F^z3Fc_(HVqL;qUCio6LD55- z#fu}HTd!z8Vh9{x1sb8>w-HNB^u@YTIxbI54HF8cHqi3ugO*3F|B))fa|TrP66v#g zI%3Yy)k1l3!&rCuPz%wVfb&ntnu3iZBQa|UU8VnyvboMU;47cL?PfGhA71?MMEpC;qdpCuF|F_a-39|ApJO=w@# z7=<~UH$=j>pV(Iq(|vrX3VKzJDR`6c0V#Rn4Z=)5J(I;lSeCjVzb&t1bb3 zRiev}p`mnXVP+FFE$#a2t+f1{q`umBI!6k@~wE8V*h~y`f2K^uFHQdjJQ^w?c>@Y=mx#v zzXU?Iq6^dm4;v|!(Dm!Nup`HBuvceeYxQv&$IKBDKT(X7)QA4bY3pA{AAi!=#cz2K z=9nI9fB~(vyLUghf1eU+O$@<&ewUzvLp%nq(+_Lg?={io#J^@nw?j_*E==KN*xj!K zla@~iTu_#ofZkF?-J&fxFmCd(;#TA5PKPkf{P0Zw+GqXw^XD^WLz^&OgIQl+|MmEY zLjE4Fd9UB!(oC?X{2?{GZezXqDfnD`1!5cIDsOVBr&8cwPo8+`EAmAYwcVS6^aZ|3 zc!5(ehg+UyHwSk3uy`+?WU1qRg|X$$X^e((^YlC*Ag~9UA!TiS5?cfbPRhue3@E&K zkdZ+n%&p$llnR5nCFC3Eqzs5MRP#@DN8 z_xyM2INGvnC-)yYvIWfnmM3eVY(5IX{MkHcJ)CCTTHo=oT8e_Cql06LMzr;_3&?Lg zd-?J)%Kj;M#M!r<0lN8t0@wIy>oBiFV(g>LaA12dr<%;Hm1D8bly5J^ccAFo5$xz& zN$=+cwJ^rJ|1%U2e5xSWNUfSc4?X$m$O)(IgS7p1aT^{p3u}7ioxgD5Ed)nZQlOO@ zE4A^*mk_=1#(`&klZRh^38j^et^EN@8ehzG>INgBK{o5UEr?OrVMp2^n zA zhvLi2S;CaYDx7-P=al=my~|R7WnLU*I84cXR6Thg-_36sMg0NC_UVh+2mR@=qp3|l zRlK94o!QGrzlOiZ@?JADQz?vz?lzj8ZTD(Jm&SWYM0fovoS$a7gbh$nzbZ!>xU&-6 zulw?pDmtyTeh*CQ@lb6L8;_fk>vY*7^MB?YI=Bj zdTl3fyBIJhTm1lF$x{q8?WnqT^4+zQWwb<({?R>8M6w~SsIydsBSkLUVs5fsGmz3N9i}7*9mbY#?WJ$hc zyf;5Pd)W&DSG%*kVK^ZF?th@u!=dRuAHQ+U!HVF3c#~6@Wj{_CuDw>>4qxKRd}ZaL z*rW!VkA?^jeCaZ$IH6@}B5!9SAc)HkPxo{D+ah=oeQ5ew2+ik2-CZ8giMDkt`)5?yIp4q}5bAJv#O{-)%%h^C-`>cM%+1~XFyik+he|qmmpn*u zadG#bJoyL}CGX^9Zi+Vu1g}s+K4s%MMP((bHVl9zw0^71wz^&NAT_2eukq}3tn7uo z@(p>syfj_>`u%q~^L@U**kf?&Ql39D>p;(Bym(x<#g=ew!5UcQ+w!qWZSG&gjxz~6 zWL(YBKmI~kjD7lCY5X21TW#7Avm?giDi;Ftj zyyH&w>CG5lXdxgdB*va*QJ6A4q(GTzq~LB+7EjW)CMPG4+7B{BZsE}@x@$jvQ8?!O zhRA<;=vi8#L1awc8p3^dBG}{eKOxSGW8e{mykE(j^qHzXPyjYt^GY|c0EX;g#3hs* z#5Zz2yh{|BEnjyhVO$L9$Wd-=pzppqXl{kGQxW|m4wdk*U3`zKb!nJ|NQE(RaTjdV zikIjrXr*Oq>1qYETt5kW-ie33Ll~Imyi7(owG{zxh1hY!+ZPqn z)F$UxTut8regCjtIw8ic7z$WqDG(rPxQ8X|HhgA+q%o+DEsu8{6Z6;`;e>;vGI9b} zZMWF0(icpxIDq4l?65S^C#m9T3nBZndcmB5`+V6qHvVmf!G^FJKQOO36`Kn=vIn?w z;faZ|nf^ZNv3-b)?$*v)>Kip?<~O&vN=ZnpyQs9hHa2}NdS94r4mWUu0+Kx)vgD3A zbR`yp>Y4qshUE{tbapNptGzetxLL18*eyjmwa~q-_iQ2hzfIo2PLqkI|7_oqTK_}Hs4D-;=v%Vmo zwEfN(h&gWCyY~?$`+|=74#9Z!OcVMY=gvZ3pTdT6Jp};;+JtE+fU_H^B+`FfCP7C>+I982tLH%*-R~leB~RorZ?a zR3Y+*8U)em7Fw;TPfzV6Ca?B6Crs;co$!UU#{lI3<3NDg&p|-WE9n=nTXf}GY+(aKZnsG`%DXe z3&Z|4M6md0#S50BH(F2222W*m44LE%2`0}`g)htw14u~Vu*!#+ydhrp{jE6h4nhrE zCSTty%zabBV&ov3ZcP&|k^jhB;mwN^qTlgOU3E1CVU*QTd^g9;Ew=d`=r3tA$%;Uj+}ZZPKXIT$XC(mu@Xg$UY7)m+CyfRYFoSzO2Vb;kEL< zI>11#5estdWUFSIjX+whbXvZ?(vVc;*ednqmW3#ve1njix`POwE z-e>68SRza^W*(050wR(!ubc0yLtXC`%1I#c^D^_K%;GPB$HX)BW^0o@T8a)N!V$oU z?{pd&%FoVNNYtF>f&*bey5hFm-{G6Wi~N?C`(nqj!Isswu(_1o0>aa8?%cE7a%`J? zezGwi0dPjoh4!b*vYkM8W~>9(fdgwn5jc^TYg_rltqbbex_)k(Zx8I@olgSa<&S5AiU&H`_<-riXQ` z?d%+#a((1u3gV(`kM97VIuRD?a`_F@T`%E=@pO!;=}$-^Qgt)U^s_3 zvEDm%&QwvNe0%~M7S`NLaac_g-8o?@&i+$0yhyM;;X7%BYbqw2)+C3&PK>-1&l}Pg z)^E{ar_Oh81jgXwXlLdwQI|DrazQ&@|MTnbY5VDK@2h(w*edu4D1K5s#qji;!lv*+ z+k6v;cG0FEMI))EC^s~!Dl>jxS-DeO^`HBqEGdbR)h-MRas`KT z1H=;fn(uE{-dXvVz+tZ1w<#MN-hTC;-&%f^Tu!^9&wlXMQ8$P6mniA;*w#s$a zzt4RPJG#1xt3v|LdhYwTxsRI!{;c4NJ3s$bpYopxB1m1`-MYGgs~Y-r*8l#lPhY)X z-Mp0)m)klfkAErlQ|>$X>;HV@|F;+Ro`j>LASM;LgS%h1pzFH4cnSyaE=tv?-}n~> zK}F>+!1a5DyD4s?i!n`EW3MGy3@8qXQNkT z$9I(k0)MYX%<`+he+@-lL&HxL?Cp9wT?7f~&r_vp&rod-v~Bve>M$i9WtZ6G9UvEQ zHSe#ToSGL#)`jZcTZ(gvQHaug^r7U`}eQ`($4BQ2* zwwuBNQ-z$NK_Js8Fw(!{oJ3&<_`4uKN7>4Jc=2Z=$dgpNmf)pK~et_3PIe zC<%oL?f^bQd^*$!MMXUR8O0q!n6P%HeFRek@p5cmwo`>x$do&b_f!Hi^@g=59-CXt zbv~=O<0ND+y3)0cZ`=TZaSJNxpGCFsc<}kxE!@Dhw27(mOzN1EY)%R(6 zY3X;sNKQo_B?ikgm-@fiE$c3&b^^!zny-gnHdt75_!yPqzeF28M?v zacV%cAmKMaph+~Ljn0yBp^s;^1hxWRopl;v`I6Losi zV}vo7M6(Mx@OI_ZdlBpw0wFU2@fLPnVnmDsxpF08ZUKLSUd9QawdqSZ z@#>?+z(fuNd>`VC;+e-)W1aW$^XKYKzM`8y_thl)Nn-Lvt?Z=?n!cu;WeiKEzI5xJ+Jc1#m?3g{-jifpM<;>A<`-tA# zlG&f2P-tEqdX#>no{0;o6^Z%0t}q&{x{{VkL`K zaWYBari_BZ^l97s+n#BbOifH!fB5j>70Pw##fC5#h}o#Tyxh}?l9=Yf{_rjos6KV_ z->km;cTC_GKU(QWcPPUNQwWha{Iqy!PG+1}h%UApANMYPU zSW!K%V{7RN)m|2m&wmwrbBY&+7XC2H-?-lmsq(vsNx+4yabOXn9NmB`L5_=i7E+F6 z=0v^~Y8X5*MZ&v8P(a`}M*Ng>N`m%u$FHEs>MsgZHhUtu0pXg3U?eDEP{6{z?_5L! zKX2?iY6(i{Nm3(uf<7kiUW^l=j;y{P7#OH4j*C?`g-j&daz}ObUodoPYM!{d4u!`> zzV53-O~Vr`O1%LFJg_3Vcfnw_$`KTWXfvYIX;K~0d!i~Qv(PPqp)3E<&+zD6{exWF zp*^spmrm{hDV7R^ospGQ)|)9HjQf86{COl{&dbV9oSq%%?|+Af?>?V{to9%}7bo|` z);>T+k5Mvm8?FxKExl&SwRJ$227w^eIYnA<5QN&NPso=q^0*rC4j?zz6; z=wn0>_vVSjk+iFodvbNa9a=WWU8f{QlFn*eP2Y_NiBEWl>ebKX z4t{__xs&b|ogycG#Og2;PBXMfw<1eFnRCsYIKa5h%KOlovW0Q$Mv6;UU@MqJ1c8H#I5By2Zz+xQoMNaf=ni#K}mmNKC(SO zao|26@6~N?jl$%Tk(3n6oyDD4&_I+|^xxY0rnji6p}_+;T4My#mN|~o$yJsZ{v)NT z>R;7}+A~*u%6h3ah>B->_QDLHx&afD>w2%BGHX7V5)~U0<6v$3;pseT^G}{SH9tAK zcAFTghL? zVDI!#?&-F}9U_?>qEP2z@a9#MQ2z!Phw8wxx#Xmzj~I3G*TlNVaO(Kqw_;aOlrhkv zg(RV#Ln=4d&H;4}pukhHsMz1jXxS$1IG4$Gch});n*mj5gy@g{{1{J)N=Vp@l|1@F zmN+KJq7gkJ_&k+ZDeVVUm6acnMY$pfE|&LpsO=$n3lvo;s;ICEQUhpAvCp^nPr#4` zO=7`nq56EmiX_tC#liE|LT@c7!zG?}(QNgEqxzeZb2CyC?6_bd z#?_mQ&CNZluenuTeGjagicvE-p2Tu(RDArJm{vm=ReTrkZ5NW6KEQgyMC?CyY#aE` zcSt7cXKLb4caZkwV!q(Y&s9}Y=u30zfCnv|R@t#0@^?6N-0!@JI42)J;WS4n3Pi}yM93}&{J1$#mJMqK&hrGZP^PA)DZL9Q6^Q4MFmm_H#!(QiBy&gQ`cKn6x=d!XfQli1%Vnkm6`@@JmOx1OD z>&_{{()XxQs%U6zLXt@>1HJUA7C*#h7oSK*_K1ZRx|48jPMkoYTQ!*~JXFYjf40`& z^4DDFhfYxl+X-9g-Fp+ZeOpo=%k1nd0g)yhU&YMCbT1!EQ}xG|9wK3r~jt;^+L6u zS$;}2w$SF*y5aZbi$``g`dGV`6=CADfkPQ#1j%b+Yd4)6)xN~X!=q4NiYy5a`1r`6 zi=53v=slu1lgwm2(k|>h!b6*KlZpRpTiX-hyIi@SA6lJ*Q83oE$MR=aeW6b=DpO$$ zG4Wh!d^}9Z{w_v6t3$!7R}@#%HRN;meA(ywjpl>gUWWs0Y@ZA= zqECz5U&QCr;HNg)NE`s=JY|xo9&VFb1Ij%?G9|~B7Vz#8&ae$vd!w+MWn1sLY{CQbfG@a1`JsUW;-cvRGfUZ&re6B^F`u(5a3zJs)gil3bkV}FaUL3o$l zM>V(@X4m$X!8_nJRkI_Q4EGM@7GRzCt&Jn*yeB(WYf&2V1wRW89tb~Oy?vkW>6`T8 zx9^0qNT;>?sZ#odgggN+-UW5xk&w_(?(FoWq{>Kfg02U6^ER7225IVDIzM0wPR7>a zQ0UnL#O_6k4MSX$P1JtMD@W9J?h$J; zuPPn>ve|j{;9}@?f9=RSz-A(`M~o2+W3!i0KnJ^|Y` zHb18{{L`-0*}(}99Y5SQEq9DgTDH%&f(5l<$3T{J_4Gub z5V?Lgt(pv0t!n_o_PV@ZbFSrQQgBnci~T6&AHOX4L6akSz+`k_ts?~o z1)g4(Tlc0|V94CFq~i~CX=!fmqBlEdZy&(7P0W72m52c8#Q@7wy4k0~QSqlcLJ98~ zl`HZ{{c~<+X8l0hC?uSa5?%Ybdd;2u1Qzt0H zTc-ygG%Mmxx*V0_|8x4CTE6#_|BJo%fT}9%xw!9#F=LCRpL8Y!)JCj45n6kMj*I) zw|v}kdNpz=Yc|f3yK3IpT$et*jZSTs=Sy_>aP$3ic2{;?6XQM&GRwigtQ&%w$y=ds z(GmG|qtfBvYJ`={a#?`|8Pf~!P>v8j)VB*AJ}eo-v<)O4p>IQwJ^bUxk6GHt`(ZTK z^lyuS0)F%;GOH$&=lao1oqN2bhtBu8FL3soQZ<170T|LS$>z*5{);31k52hGe4r7( zH2sGG%@Jo>w22%NwHeRBa=_z@RW*nATMv6ooj@9tSZ8#^w!PyzmpmWRyzB@2l%69q zKlz?r|K*G4b^0k9k})!&GEVt0NzX<4m5~)l?ll{l^l-h|k)-K)Ll66?6u~Ky*+(y4 zylxFd1ba0$HX`Df`D7ttH^Mxf`Lg5Lp3%mBF-!y@KhttrjeY?-1zELBG<+M2)%;`X zM7GSe@Sq?sFzEK6z}|I2V6YnjKl_Iz^U@P>RK)DFWF()arBokQ7*7toykbnocyz%@kBGHyhp1!_{XU{$&-vVlKZNu3wIZ1oB-0%I9FOiA)GH>Ksx=0jb{;;zx zR?iXGx9^_SEZWC(KE?!ShC=Lro*hMs0(> zuz32YS4OrlUUv8``3Jb*%h5O5?=nQTANMCbme;u1{KaHH!OOE?nqSi#TztVnbm~HU z0GprCC@usw6{HJgp$l=GiH}tC*l*2mh7x1gnWXvv_a2R7{#M}UfO5$4X{Vi}hs#Bb z@&1#dq)mHBZ_hVV==<(UR8&BCg=CLZYxHHY(96@aR`aU`9(hFXOCAXcRupu`y9O`p zkn=pte);@^{KfH8{{3cvVmx0{zum)5pp<*uzx&DQKk9_6!hdrYN4Y>oB0SktMC0h8JG;@p&>5bMKN{jN^l;0oq5$#3*&v)^#zSJp1Jq z`46djwG!s;e3I%gjSuJ1J0Av8Vg)?*uUwv50#o|**)yBxK!9NRf%rGDGdq7{lJgXv zOEb|s*!rH2A%!bJoJ%R5rU;1X`aAc>EdB>y^z%qchYA@LBiJ$8IKn@{$Ax4bi>NbnK4g z?sZZHEyDv+p2^CZ9-B{5XYv$LHQhP*FumW`&yOC$ex=Sa;zl*#0JPO;$X}QH#{90&#zsLU8XfMZG8U1pEHAnT-kL=5Yr1S^x(q=> z$(MUGT`P!eUd!=0`3d7X7x!8@$@9%POrRuitXZIR$JKlj;|qZGn?4H3QS~#>M$UU= zF#dwL1)0v2Sd=bi$BrK##u*2ij4Xw~N03+uuasS(+9iGnu>>^#fZqjC#c*JxC~``^ z^9mI@5RuRj*M?6#ZWsn!{Of!-&7>4*)2(46t=mlM&o#GQ=Zs^#i53a*5=9<<)nQV0 zM^;3Hh1D9LWTtAII6;lDz>nz2yH_LXKnZheR1}YxR6jxum=mNOa^~hjpTcGQ;cH>F zZ34w+as-L#xcDiNM-f612KkWBp7$Y7-Hhk?;nk_YAMEE9VtXN;Gix<8FyJK$Gm-d; z?NAKK^zyR+RzzTWM}&o;p)1x;i9qI)TF+~eG;C{k1AFx8S!n+ug7pRFBGys%zBm=} zBp~S!DZ7h)j(v%L**1aU_z4g>k%@_P`!BwE7;R1Qee3jhb4A_nICC}ggQ+!lP<;u& zfEfW8Qj&tjahgAdHh)7k1X95)-FS`76~-db>FHaEpP(y$!Y_@SRw!;(l6vUH<0HeR$+PaEgoAq8^@}&iE##sRNpJ0r=AZ1=n$%_Nn(M z5+@r>j#v*?U{77!4sPXLX0bNXZ%}G#7W@4K^bg`A^bu}uMnhd9<9np)oPNd>78_0s zY}=*Id$QgzrU1pCh$a(tHV0Un7|Y%tD7AHk#@Xn`jY0(UJx`B}U11`U?R;$YUve~p zLt3|%1e{rFiR!?#jEv%McRrepGRv)zS`!e(hp!J%RRDb3wQ`Z9y39t{+BJEBIZzia z3V9*E%(}{D8IJ@Uo-bRW~RP-cR?l!fzHEQ7MY-Jel+a7n_#44 zUUO#mlb?)tolEu4YBh{ah2;-6Q1i zx*nZ7E-B##QG--WRPAK%{{r^wM|p(C$Chw2LGVjcwYK{g2o$b{=^Bg*#76{0xLslI z$(prk`rpnsaSfw)M|{mYjRWznS%CsN!%WiNSCVu51NI#}_$=J;@C#K<%>ye+nnRZ1 zN?%+3O3H7^%e!cD0#D|WkZ9{Y5hp_yZW=dhTKc5@b_+AJP3Yrq`CQYgg!>?FaH$l( zC?nGqtyx)7abe&oA&Tw47q5L@ODj=4ruWAWBFANJw6L_)hh2+bav=Wh4zSxW2IDdd z@i71)04;Zb!yeGd&xKVTNGmFKXQ&(^q=NUuUVn#KQ+q}RANa%Zb;GdMiBqSFK~~yG zF3Zf@N?I!3lMaXc%-Fvi{9FKc*ik>P+U~cvY6$+*ESQdR9ReN#Lv6^ujXi!48IxUT zHf7ulyJE08lB)YeEQKu|F!G`F#dk`U}Rhk@M(~UwPy`m5oN-5=K z)pc|nJkIDn(*fH6f=DuO>f$oM==ihQ@U58+3+elyJJ)#f@Zo(TzUY+T{84%1vXEnx}{!1=dqZ#?f^GwlG#mlokt^ke;W9;GEBz0 zi>(qqk+ReSD{6$Pre}n>^7~i$n$bDlK?Z`Y)yXh#6hGlGKl%D+vV%@0gN{`%4p3eg zqYSX%TjWpAAYWRrFw479zdQU^_iQDyDdDEm<~`_j5~MzEw||=z_76bv>B~wOKEuKN zU@KDe@q}EhF7>arrPD}frDM5gi&7np#CUC9S!R^Up`UeM?X2QrN9*Phvc* z3bjenPPk8wfjtkG32fQlV#~)5j34E)-deMksVvWVlwLb^>eO8zO#}j^0ibzHsrgb^ zH3{^y3z%Nb4ImP@J>ip)0z^C<#?t`*>n>9p-7Y*{XcW(?-dZ3(fFJ#6C&fp0pNUHvf|auT3}nS{jX!3 zgHP{v#xVLD0Fdpdj%#O4W1} zI0{ONitCTdZkBtnaaEf0gZV397g{Sw7c_DcRr>siJQZH^40931c)q-r9MyK(|5EMz zzi5*#c)U(bR0L=|X)8tOr&dK&{SSr3#I^$tTNmJYh_sO#&SA$SrNZjZh`(C5!Hr6A zWl_n;F+w{l)935<*DLqFN|Qk#FaFh-#C^ddxoya$Qhfzund;T{qwmyJe-Rd_bJd?%18cvse9>4juG!cnG>n{ z`;GD2l=>04Y$lfuRpPUMe|w#vDl~K+g(iG|e`oJ2XBIx%NVyVS(JlYHwC)(~7B^LH z{bbgS|Ge}D7v)CWsbDjm>p!=7gmIlK?vQ2B?Y}PXe`xh;yEZxwBxW2!)gs!q6NEC% z?0D7-AwwQ%X~Ir;`1_O5L4ROuhNu}~0!5DylHS(fpU2QYD!lp#moJV=jO4lyuSAuV zX~B}nprr)SyuWYC76p;aU`0z=O>>>mjDQ;-gF>|IUn9L$FV?}n0e1g)Z|NL2h+P-;;o%u= zy_KYgS+@vbQLsuI`OgO4k-WP1DbAHcWNrN{CeWaZe%AIxhsLXfWAJX)8W$WcoOHZH~^okAZ7Z@df=Zm zcaL5D)X9toi5K@Hh950sHT`pCD#=MUX@okiqyWJ!173+YwaMKO+kN)$b96*?^?3A| zt|yn36`5otY@vBbE-l3;S4w=fVeNm4``>#$hW6h)UR~k)DF$TyA`-PrC*$8YaBjXv zUZ6+lpCGhmSzs?Zn3iDjm{K;f;baL{Y59z3Kr5-^m zfQZ0LT=-88s)!Vz0>bVi>b+6i<}W>O^f}`o%k(R{&(0Qj65#kp=YsIS;-+8Ypu9vn z0ZEKY{?gJInZ0gI8qY^`#&>S8E~0!a13 zZ+E_ae8dZN4mrZ~$svAzdXUH{iyCU`ihYIY(vfTM|7g>)esVs{&lviC4A@-nUhE78 zSPy0Hirv=xRvC-#(x)V1one zhKkPCKj_|~K0}ZgEGvk*EwP}aw#>V{BF4>JD=Xloi@x3A0%g$GW&~38(Idf#kt~xA zaa=LoN4rqIl|TGHm436#W3H`Wb(n9<==3Qmj?qXH{iM`wvd(8@dSv$Z(b3tlY9yTY z&%zQF5^I9zftD$(z(@#F2R98{;{`kR-B+~p1Arc40U<47x6R|gdO0`TEVUcS+q2YE zuq@n1XL(2~*IE$bml&0B>Atq_CS2TT`7yzXI&i>Pua~94y<@$I;vocAkhmSvE25ZN zgfbx6s5VO4lpJiDzt<)BTMQU^EC%{%4323kD3m-jW_519!WvOHfxhOh_(`G5hP(5V zH$W)@_%O=;cs8d_>7&2$r+m=cjA)-N6ncq$?e1nN210yp_dGmXPIeLpzEj zGpJ1jybFYA-RK>!C@U+QB->CjmSoW0OjX@`z3RC$s__ZaOeihx-c6zPn7<-`T}23! zu-edL3KW>5AZ<|_)e%`YC6Kwe;}v2DE0DT^ZFbBM?XurYsI>wPUU>+4hpG)#{dclK zaiFOIIevR{?mh9R+ojs(I@4-rW4_)F>Ad!#&{t^eQQpj<@++*^{j=y2_6R!*bJ6Y5 z*;FIV=ut=z-!BPK(QT;rC+-P5$`2yCs$fwDEFw|u{^8r3o7M(J}&%t zqLK_A0f6IlHv79bb#+8P2CP4%WD3JM`RUO~^EYGj7tzb?>z$)0T5~6O5=r7TeaBGt z50L-j`zWpHj}5PwW?^Q2H6M&5j~Bryun6$R!bBzvKVLo_iWlRJ2}-;53QDic%*=>S zei{%^F=|uwTo8@Ml2S0Jx#8}y=TJWomx&5XIq+ObZDSZSU4FZ1M=A5i<-@R7#4$CT zsUFXv?+g~%fpg2f%hLYX?fa_%K>m?cKZJ+@I|!DlvR2vFp~i^sxA$*QvlLu!it)27 zGRqCpYy3St9-eQ|#0UWK-KU@5$9xBSOeBWw+JH7kcSgU;_DxBJijTDd6G4B4udp@m z?AhrzK=bn@tW8B&_F7uipH*lJB_0>6YU0siNS!}g81T;8rO+m>vL z1hUmKS|Kpu%wkBwHyG$UN7>>JhvUX^r^$9RCSsxIeRiN=x(IpyT?nlS#V_bejV!Zk z*egj*21DEGNBcC{T9q%K&APFhBh*{IF?VG{p0C}QvsW>#`*IufGe2=ji9=W)C+vN)Tm} zIg&t4aKCq8v!K3YL6gk4)jhW*U|vzcN-P9~JP?U|#T%P_iOU%3^UrU6EdW#CjfV*9 z8G_+QQ|&Ms$bbCtrpB#l)DSTpCS8WTO*d{?i(@KiwogVzC!hfZrK&nQ0+lE2n;t*j z6msh0HPesp5PQO`B0L9zqt63&N`HEuy%(ija!<~*P4HU97JHyRaY;!t zRAn3Voi2@+UzHT!=AO|xDe#$nDXEEAvcr#8bP5K-SGQ+89g)89eD)Gyp+zJxsu9|!JtAB;nJzGp`M7i>SN9{LmPmN$`!j4fX3^XK2Hnh znA0zKis=PyF?3wfjUD`T5pYt%DG3^s!Q9DO^1ykzV*|H%5_5uSBk4J%eb6P{j&zD= z5t=xf$@#!<=SEs8}`8mY>?f% zDPdTf-(B-=r`PHERxMhV>*kl564w>^k*q+43G=r1LVQb0d3Qg*K7G3)<{~TOR5Lab z(hs?$CeUe{V4?$hriU`RDw?w0R1pyq;TE=IkCeZdAz&gSQ*7Y7G0bRO$AadDrLptUv_yrtcC!<48cq&y0-ui4A9P=w+z zUAQ+Oou6WcqM+8%~?0buvkz9Jm{X=K%D6@1Flke$C0Gkw4qV7gf_BM(Lc~kvRvVf~>`?C)y}iAa z$!OF=v*qDRp`djN2Ac@5fGeE_*>Zvy!+N6p-M zfzArUI#SbEL+p9Us)gAeo~g@2U&7x33!RZ_jbd4|OS?Xh@fR2Yjv;BZQ=5$;aGkUBaZ9KV^`A~|ds(=;#!H)%%~r@f0P66jGNu}2jfy}x{|8M?LZF>RQw zG6Z^P2}Sf?dOgd^bn*0y!|Tz1>oNBQ^rX8P9jMe=)|=5#rVLgtQCepzZYZ4d$%q$7P!AQRjOdD z$O}UX-JWF2R5OE~A8VfXae3$T9z+)%O3TaP4Nc>W-hAFux1>+mooANopOUKgHd~gU z7FAa0E@TjN>`K>8pd&m4fZJzDOtK!gzV=?e&O<`W0DlNZng*^>HFS-m^+7=WA7o!v zBK}9TH`nfmGzWGeu4sRxOxw4!GpWnIr^rX` ze4NQ2=_;^d^zOwYj5pzsIl{S>ByeVJ>y)G&Ga=QOVROZW4gzami&A)O9=F#P>& zJALFuDq+S2Zz)?gY}JVcejCeMHPX+*N=j!}Gv5srtKioBl|qPa(M69yBwl{!w|cVs z%;Oc5wMY^u!=e=D?e>!+=H6$mc`O*?Z}YC1z@-dpqx6s^Rj@7k`g zv$BfPr&RaDCb@__sUv&R?tUoSc}zbdGUW!_X=5(RwPgbC6y0SM#+2T&`WCx?wM%P_ zWT|CMgX?~)c*Lo2VmTr;@R9H#pX3uR6$0@sf+u|=;$FR{D0KF zaCTk;7QcANvT{^+W<#@){WjxS#~!&a3{$p*h}08<=PA~%fw=IaY5`xxvK*g3nK#g| z)mUV9$r1WJlkDLOiuktfamf(|i*(d*eWX2O~DvK=I0OqeXH(VJ)ewl0J1e3O*a4HJrO z?G_*3H!$FW8m{7R$G8|j6cw#SgAHe-E4es$r$B4XD2sAZw52q@sTCElyRS| zMKgWl%)Qn#D+8HL1FkVQM(6AiLph#$97U4W7|f?D*eft8YdEufI2-CCJPQgJS**R7 z!LjfZ#)E@|-bG+qjwG_!Ak&PQhHu`y(Y%INiF7@NkHYdUE z6^+bPzDl0oVCi6r3F5oA`{4>dz6x*`Lw~=ovc20EXU{M%&p+D`o)%uXa>>fzYvcAG zS2adue#+~dEl3?ImB~*ia`_}=*Qt{$7$J~YpClnEd7y0t?RvNwsXjh5w6WQa7BD+p&!!fcv>7eQt)qz?@$Y7`_)@E7^Y<-tnTp*kXHKb#*kFcBHFSg5xjd8u* z5tEF+4moT}((ya>CSS5@uK8Agd$CF9%;DT6KW8I)yI}@f>41O$h{m^0j7{HkFV*)H zbMyZtd>ti+jwNBIj^FiTupOS7;2e=W9?YE?-s3|Sla8g%c%5i;IiS#ctuzO*5pZjHY3!NDu9q z1%b-3B`+E)>5pPxh62g*r_`;|^FFsvzm(y`ibRQq*6LF|i#R!bjNA6fQ8vH0%AvlY z?lEiK3?a6N?$S%D04NlW7q70{F1rY{tXTcpu9wClw=3?klqycy>X*->;9`bf2UVxJ8<%l&>A zg{!20uJHG}@Ek7s|93ej{mf#Sr9@L}e_Q6{ zRM0xLYa?v)7D2&!g;P2qWuXja56#9E>cSa_yRNH4>cy6iFPj1;S51B=)Uv3+MR*D@2lg=H8IQdyjhh38YkLJClSum_2*P{ z4cokJtMA6O1xh5&>J|Hj-WJGTxQ)tO=klv^Dk=;jqN3cS4}*gT>M+)^)6v?R1*M_m zy%>HgN97DlVc;|z6a#GX+n*!m78cBC&h)2k0mm2Fd1I)E4)EnVtw=9AI?1)Rz;aFJ zTWM-5Z#BS2a*DLydG?aMFFkAf*2V6eeTy~;n+P9dA1U0AzSsD=$F};o8}uYHQ30SF zyBRqh3adUJp~K(M20&k`vS8yrR!ghkHh3qagU-|NL5u;M07N-@y{v~*v#LG5Ney@ zyu3WvF7djBs*fM z*{o&vy@c~yr1sb??aO98a+ygl?@=wUScXro4xg8%s*N@Il;y8DP--g&=o-Xd;D1qB zo&43?S>#L`)zg8spOlKDa3{X`%BT#ki;721o zw=?K@i_$wDw1t8~ijtD@kdzcr8}c$He`D=|tu*j%qtHRN=U6d-^X^Dx$GDF40!Tul zmiPl{u=iMY?C?73h?+}UdU}0(dp7757U)x2Sq}@gKXdQ5b7h&c2n*4gp-Eyt&lz6Y z*!${vzPg9G;koJDq`dhzQBi9NFNto+T%=>@a)^?&9`m=jr<{E3ca)t#xC>kBhUQnv z+x5pj9Z!>nb^Pz)4g*%jmD4)Dqw_!lW`$Iv1zF`~d&nD4Slm{#)t2#T`tGk+phmpe{Jewt3 zsU=@JgVV7xeIU(#B-S)xw4urd5#3qq-Rt{ft7a=l_Kg+0v91{8v&lGi7j9b@`6^JD zS!8k6cLTy7m(sI)mRas}moI%znLK}eF>v%N*))E1sQ$3MusO%Tii#&~Bw;}eKKMG> zU37!3;NL>ldGI4SI0g5&XULGp4?+?uZZ6V?uU~bWQ-Ir|n2L*$ifFM(#E0BpfV2C! zx~`|9u`)H z5iIcgKsC7AGOj>PhYkWD1XJF>ckS=L@jFN;SLMAZfn~(r!dpU%qS2O+kN_k0{W}i` z{Ed)zR316w!nhWJC&I{LRG8(<#U<9rcmL`tI1p%Xpk7aDeyP%5kyAOb9o?(N%jQ0pT|K#1<$Mi1)p7}tH^dgUTk_wHc< z6hcIIN|U5&Rkn{q&}ASuyrn|Jrk0>XOh7RognrO zh5{q@MAP5h(>Hj&J@2szqsFl=Xv$U`OD^4J)ZU-;_dlCt@$QR4|W+J zwaeBvF4}ZB(OnfAwKTqLR-hLcgnWgO%<06;N$S(9AI-U(tUC_^nQgUB_vu2l=aYYD}3p30IGyBh*(hP z;&1oyr&Qm$vgZM#+%Pk`$YI|#B&c_%%NKM(`RKuT)7EweLVlbg6l-IN$Hh>_b&SgS z=g*%GHTSd6S?oLTN3LoJB=+hj`nqb4dq3G`F@sp6Q-h z7(b!elEYKB$b=qEu34!0?vhQ516`bdBO#CbJmsnRYb#x+w!y8+Bd~tT!L`?m z$8@1b-ms)99_RDixh?qRvB^R)o8S)ar?a_RRt!7XYel3;^9@t#3u;RU?qNH-rDJWY znS{Tzo?*}+F(O>O7T>bMAa8xkoxGuA0yX&8Z zp-JCTMOUzcH(_(>xb4E2ZFs1Z-BvlO)ih3g12xh5o$c=1@1g#A^9NePRAK%PYn*%f zc_Ex}?e9De(1z|IkIqhQv}h5#uKVJNULm4)&M(&nZI+jF)wZ{Owig#LfKUm!>bF5SqjY zLxE-+}{HE);;+h+dY_WIj0y{hK!BD)JL`|oreJ+ZprF@MLZXdy$F)Z`5>exj>uFN#~2 z7a()RN56Ox$yL}r77-Ie326tAp!j&V>dA<1=7Xenm(Hwus}Y@m=-1On!rBE&KBcRh zD0A#5)dh|vDsSoT2s&?M`mty-PxW)!QrqFh*e=(%td{_etR;NZc;sfU@qLq~c=RZrVJ@;q)ZC+X#>dZ3|Jt?nU|q(o(-toXgV^jslUn388CDZXcG`o5b{RjnI5ywu=Im0_11uv@=7k_8;b9+vPqj61 zY`-kIFv$kz@_|4eN-NZbR0w!tq#$ReE_yiTvs<-Pb&Fo0sE9}fT~t$JjvB8qCsHlOt`T5S?VN=Ic zjRP#4oPp%Y1*AQ%wGYh1eY zhzx}3+QZG>EpK!!qBN$RMmC6#c`-@;x&5M!a};| zujy}+W;;Qs?G5fQ%+1>{?HZBfPO8BCO9fcpK< z`TMV;G24lPn*wEBZZBSJMNoHF1t8s4Iig3v}D@*LR4gJf8=3K2jqY{eIgx?FYRVG_oIrY+2Nc#3-ICt`n|}FT*#l z?$+Pnp19X1kr$^`)tDjwVIrd>D&oct6h*C=IYMC&7hkapeXS!SxdD8Gr|3ox7A7)E zct4}A0JT3<+=q@Fp?FeGLQVWVOUv(=v*MF^1}6|rD?l?HLX$dAPiiqsqT>rXw4OeF z8Y50l>1CkZR-kP0*RNds+O*XiCy;+5$AX7LhbJ$f1vY41@c?g6boWIAaT*$hsQ0`m zWdPr)bV`Dd+y<9c&ctvQimCD z&Z_N_UR3b53*YrDt8aGlu-QiCp*fB0r%(2bBEj#f3UJkO%>V3F{d&`v3!_UR#NORh z^BcfcG{pE}V1;6BC(g>HMq;v+!KZtu#>Oe-+qduJyzZ;$-Z5(=V2o>l*65MZs19lh zxS{)?3-oW?IN_#B$OX4<-O5FJSmO9Y>n3VJ!T-s;S>8bhC$f0*?#}`U=zRpHN%^d-FpeXzXEBd9Wi8xSl zROl67h{{t;^$lYH#N8OauzejG$)#T^7)Gt`-gONcLpb7yh7MEYp_G%vC>R*U;TrPb zJaIa__M7wgH7vfP8Y&LdeIM#`blN-knh|F<#VN135B`_TM1=LHgInH2d>g;<@>LmU z2I1Ef$c3xtWqbeC*!rwqGx3}b`ZM)K0XzFSo2M8eCZ|DAJ`y^OmZ<9$&;A(PwnSvC z^9u_-HS=S@hZJBw!R=ut%mD!bvawFu(*4P6SF^H`u0KE*Pd1=WciRBr{p{@}T2+fx z#(<3)o-4oid~6`Ng-LwJOy^T6Z3+D$>R0ZnIr5J>GQ2@lb(w6VrTjK!JBAsJ`g(e6 zL=vd6k?wVT^lSNqD&Uqq^MkMR%8unS-n2@N&B%j^{Kdj2AP{J9=py5m+qBS{$Wbw+ zrws#DGgDH3V~I5;2xJ#oJo*Ft{4jHa%{qqaGG^ZzzwSbpd7C%4k0}~h&DM$AWizvf zYr8Fsf1B>8C{HE;A%hyUu52EJUo{=)UCN6Y&-W7o>5}~c`hr~XPLe+&@e9qQ8Op7&ku$-xoAs~ zY#l~;wcaqE3}@OxOL_L(IUadF%;^EZ{e7&XSx*Tm@S>v(p4qj6VVDG0RcMKQ=j_p? zZ;K%1r0UtO;+rzyahDqF>v;w2Ci@=Ib`uvMXZH|xp>Sk|bHJ>sqiXBsR(Jky+e9mQ z{Cb#OMfM6vhVK=~X}Q9lvVBh0h@Ta4I8)bSLlMrAiJgRF^(y^shtpj;Ohm!PV*uvM zCwt^iNu%#VPk{G~*f^u?V0k#+ur|94~ajM}UZQCvTR#9D#Y_BC&P%l$VGuEk_c~2MJnffN&wk=EF(!uPxeOl zvw8w1XBn=*e3sSdKaKM@F_X-$siuadFX~I(il+jMcr|t%Z%Y8;aq?tEzM*Pua6(E7 z({H>MT`lF+)dNFbU=-*Bbk?e*OXY<2n}BZHel0BYEim*Wuhc?b`J=35|Ni}YGfnO7 zKGoG{Y{GL25^K4<6jd2R#{xocZ4FVNAi7uls#)pe?3**14WuRdI2#xkm>JB3 zg>i@p-AWQunKMfpA zG**+Lmpbx*4glut#Km34jV|tNrY`6esX4s<)s4#~V?;cLjW?>Ybx&U6hN*w}bdSZY z(m2JF36hIb`&CM$&#rnLPu+ir@lHy>))12duJO(K;@jzn3Jl`kIqjM6CWmNm)Tz99 z|l z5Y%)*CT0}mhQHzcQ0Nb}EXrF5z66mDU`RwqkhvllMmT}ijn47$;89L+Ca*pr;K(Q$ee;j^!5t>uX-29oLW<)C|L-U*>mMBR$RuP=MC)&Si3yw<9zp>PFx(!+oN$}{zMepbEscW-lN zm8GJIHa!N`16Kh|6~5xqr5Cwcva)MHax6iKkSWIrA}U<;e*`XqjeSZ+ zETek<{5tF#1T15+?wuTvix5ueVmQ;(>`1`@LaMdWZ}rW8%}l=Z^VT=J?HAH$31_oc zgF3h&t@k$0B);m1ZpOIZ68|o1BI~%L&hc*rF5R6fXjKkN!bC*@6+-k&nIsML4N1w( zrRAnTgaXtxbW&v3DHU^(D*g14;CK!Sq0JEwxjeQVRaQ6?LjDa3SgUy1&dT>ugNeKnwXK>0W+hX9RK%e#> zWB-9ASwwL4za#qrF9BY7V&0qxexov;teU2Ok1Exv!bq#B_2@JLLV|&VJmZP9Zoko# z<3pE6I=y;7nr!}*Va{h00Ar2q491#9{X}bi;GU(JT^q32Uu;zr*(R(d`*5dVmqAYc zcz*Iffk(j@0-ctYhK?z$Aa2p{ss!ONShUYAE!2>(0IEXFN^*Vm-zq(3jE`58Zzj|a zLtO!iX%0q4RL;l#Ks21`wBlGYhL&0F1x}oZ4k0U`2@YX7{I?b$_!7&0(!0xv=*1&z zXt)PK9sAauL=PgsE1(vRLt}GbYh}J4`-N4c3iBU607okJ>p;^Vcm$N*f%C>N%V#4r z5K00A1#^TBBT9H3qEvFXEM5lm^v=q%114}le*<|D-$^4+eF$21@7Yt^-p1;vNxZ(RFHP=&t;a~&<8ssA7KnM@Dzem#u z1FMO>5UW^#ftc0q^^gL4?223UhsXcQW&XyLo>wOoxxuNTJgo?kIVzBgx5kpCjD*TE4uzPeB^FF!mYf_}ilrO@ge zf`ka!?bWMSKVDPJfxJQXtX%iS0@T2+LBAFxmJf=*Jq%n}ay)SG;BN#92I}94E6~|~ z=Qcc>1j212+gD+{s3`=Bhs7#D3%JkR|x%0h*8Wdhen=h6eQHB2=JAn14PDwEdli)=r z-@9EGW)3?c8&jOy>!=FNBhELaNvP*h2>&S{u6bz5X?~Oq+nQWe<>VALzl}7J0HR5Z){}iqzq?WT**{^-P zp@a9tqbXH$T81o*f_BUN05FFFix1sngdG6a7eYN1;KfXI8)o=*_`BQTq|{4i2oErz zG`anJxc#_i2s>=)rt}o6H=y_#I&xg7p)io=bLf9oe)Vx-5w<1vPR|thASgdplWfU+ zyRA+u+53jgzsN^)oQlHttwyw~+7xIakyc&pd&&2WXmM7&O~;;qQ$<7&B=EXCwI=&~ zmgnLog}euw?hdH9Vt_zU!RCYwbkx{bQN41i)IP*QY8og-HwHJrvm{A)4cerY3 z?w~fND?kc5*fwzw!m}S?s-`K?w2|NhbR5+^$yPOGlEZh7j(&6JM4#PtU<}5`0Z$#* z+C+eJGoE!L$HVVpR1BCe#9Zpao$sthIR*CdfDKy9nbD&ll`g4GYViCXM+Wn; zc_IaUv}mg*rApvcc&AC{wyY1T+&y!zt%l`ud!%nU!?^3(Rj;t&x4;__4diG@YtTBg z4%M-de3AE0uFRmsAHMytkbLE_$x331@uf@Urer(se(nn^?paKn7a2Co-9G(HsiP_j zt`GbUfEd8v+rgLZp1E=E=_q4)?*vZ&4zG7sR1K(D1PFy;gca8=L3V@j6?^$HWdkI` z?9#GS+u7kX2qwKZus^D#MxO{m@iB9vuHc07*)7{`r)Fk|Zh~MJ^&*FG5}>B}!J&bz zkeA0UM@979!=pAE;X$VUpY$22y@F`wZryW)2nF48#-|%}h&l&E6McPrU>>!!FldQl zi=yXt;&2M&&#onUVwIZIRpU$W;8N>A0ig?SNlF)Sy%U1GGl(O z$?$sFO*|#fCclo}pXyEWcKaGGl!Ph9^KE3a6neYz{oaLZpT5Ke7F;pk*h`am*8iDA zw^MWnJ{X4$z)u_3UioiBC=4YhhVxc7pPpYS9 zZjyf~2re&Ms3~wYp~teQn9g6onv}smr7@>WN*o~hwP^#_C-HbXA9}()EjRsgb(m_xz#0&al&c(5VzTKbd!(t& zo$AHK#b!3auu2sb8$qUwf`X$33w`tEM!fNO+qxx388Be0h`XibR;yfs6{5_edsB69C}fG9T824cxlqF7?+$0 zStI!gs3AUIV9nohp-Vxs@%BN4kJCr%J1`2&HaUQs$jrm*$0& zrBE-CU~#J)z7ww0<6nwPWQaj#@$nqxqQ!Y~YB7RCBu9h88K7l5*mJE9-y(h4exi-D~ z=PCfOc}5)gHp>1V%3#_&?7~a`HR96q15oaL&`;Spc=x<=*s;8yA0Tko3k?5s(uG%O z=pzT&Qk*$9wNCeN=%4dh(zJlC;n=wM@4wZR0X|<5y%p_cBu${_1+htOmm7T*&1G=l zCM^}7gopbhav;gP1tS+zLQsv1o~>+;i@|An^_jmTXO`uxZe;7D`oBL z?=NFC&3YgW=g*r*tQ^{0S_i*^x-lwl5X|?9ir8*MC5&=kF){82@9b7&aso9DZkpC)q4oDwB8fN3PHQg3o1tOZS;pj8UL2tS12OBhFI5WED=9}fH zP>;rgT=bU(WNSo61*iOfEu5KJ`gvyCZQ4b&zZj>0jm10k-`o8|u7JbredZtA|=MP zNIP&_<4b&)yo-v?e3d?I2#k8`sliWh@#9k_Pz@X!X4I7|~*nF2Y z6a;TyAC;ZtlEdzc5||RTft#Wgz3#iGr>68Ej(`1nOr{-qAZRQ8!KJfjy%_-Bj{~0-v<&eSHOA;fLNj|moIDP>ONou7 zrC3WOA_z84a9SNd*vc}SRSsH7gQ~$+m`HBlNbx&Y?+`POahTn7c_c5$!SLTIfSOYk zI}5J3_^J4=?78C9fvI*FOMc}yiUnE;%g%T3U@7V~4!FETqjO;8Ad|qk0?z$i=Q=qv zgXH!9>g`LQsoeXvJE=tzc6QeLwGQEtphr{$eShlg&C1hFv+ zDD;Gc?e1B%`(7jM0k1zpVS`TqV$vvO301UwY|e93cktHz0E}f50u_lBg^#ZpWEkY3 zv7towEv3bzjdqh$TF!mxj#WS0O8R)Hw23aQ%5=?)nS>StN}N^mfq%qaMBPPGpMc=V zuQ~BO)AF45gAM1aESYLu?l93Jf@485FS*bO%ZhW^UW#{O z$<2a*43Ws-PcU%w_1mEm7|lkfi((qh$q)9k7M`9tTql9E5c!9{gO)(VMQ-aNm4iUD z0Kx$lK-sKiU_haVtx6&kPmrT>(dO8JSW;1u*7Z5G5C!Ewm|obud%|v9Sdjk}jyK`` z3{q*pU_?wASX=mkTx?H+E{LgW2)Jfwg>;*DMK6WX1~*_nVr5$Ijc@m{8THH~bh@5i zUeI|3K)U3Co`MIU4Ds18=8uF88&-P_*w^SLcV==EVnSGw0FGt%P zh1sz8NRi;;$Ud>Ez9Es|7n8J}H*fxgOsi})F1$jp=J2lT{d^{>(NVl^8r-xZ$%+Uz z0P<9@!p*^9^HhCb8V}(T08Ej9LDZ*z@K5Vp22#-uyM)vao+l*q^q~NU(^hR*$nlKjtfeW6X9TjL{e-?Agk+& z9iB~A9x9vOMwNtKBFJUGMEiWyEDQR_bGxMd z3c3lp_$-7u?X|W!4GLZylw1rM`Sx0)kO?ojTVwrhtxq?Q#~<9tu00g|NokL@?!s&H z)yR9weAzRGA`9IzcFU<1JnWR{ddBuOSe8C2A-JgQ^kA+Cc0a_EJN2}WWnEhxJX_g{ z{DX$ZD5+r6N0$ZR2nDlWh(u7heU1az#!rHtukAuEfpC8z68`>Trxk{(d5%dRyAq%- zkqFaYxtFzS=k^HpX%zit)0LEPz|&wIQ<+H(dh+(|P9SKYMFM^XNM-Nncnr8SPDYTg zB0v3MRZ~^9M&t7$F-*f{B7IlZ$O?cfl6n91WZJ(Jvx%#p=oQ^K-tgt~XBJ~@^vp7_ z@qP>F=mxP(j>S}}GQ=qKIk4mj#l4@b?l2cSW+XJ z@C=aS%F`rI-CVb0^=jJTXj8#HpP7%$mgWcQMQ1yE+L=}yBzXbHAkFX-X}S+Dk~m(P zrfH@Lm^_vgx~I6=Y=f--j&d{Ey`;^n%TwNNp1Uw^{vV_rMyKls+9sS*j^~^n9O$0b zn{14V%FpB5v_(Eg>gOx@GouOD%}l})_FRv4dN1`ZMPYr}Q`yeK?NY@bew-uoVTcdK z3$DJ)?&g##xp3<6E@u9`-tEuX)_;=wQFoE1^N4oJDWbimInr-%$)KBD1oh`JWMEx8 z9@$(t;&0KN^ZE#*?eVz#24(gOY6X7IN)zK$&Bvkg&4uCT99?+_!YjWeHBK8j6xSKJ z9XnYUa4&jk3T?|0hpjK+9J1?VW( zMbtb6tbE3As@53^6m4wMHlnxPRxc_#lhKY7k;sC?A8p*_(*Vpi?EZZ;R9qa3J#vDI zV&6=fdh9k06{H-h1*aUS+5((UpbEa?g;mtquCF4~ksrTTj^HLN3^c@I#a~*CuR6hT z9=}Hbhy^RX@+_w*zu?>#u<|hqU(7&Iy`BZ0@+x&KT;IQ}!eA|MN(sken2ELin*P!E z`OB9!+PLH&9z;wxD13#ADUO0ZG@g~)R*M=F!xDPsJc+y- zPqd`?o*<41UEDR}CAyBhXfn^i$tm(JsnyBqQHSlL4jtq33l7|}u;Jk63!-uxMGP08 zw@7FqTyE;vamrNgPCj7wzQZ=VjJ~;E68gG{B(SiFOea4}*M6S!s^1GLQQYm;*S{5w zfHBiRLr)EY!RPlZ%Ytyy#S2nYbswICE}!CVE-EaHQSep!XakKI0T9`@!4d6!hv%aU z3u}U`J`C-B_#$@eGuhno{`&kUG*4bCTVk?p>em3--g_YxYU%7j8o!u}pZv@tVSDTh zRNzi7wj=&?F4HTA+MHsxGWQ2>f^z;ayMZ=S&0?ljQg4BY{IN7?n}!GDdj{CWXE)B1 za1kCI!#SXPgOEpvoj}_`S(j}t2W~mMMbT3{OibLfzyMb6ElKNR%Rj|2<4icLr`8uR z1X~QH_StdApc)`C#aXCQZm#idV?jb}qF7EZUqvBD`757=?kxyG#B+1Dv08bN8#T+2P)1k5A7)2{lxZ`?pNRusN2E+%fB0kF+LZ&G z8s{H$o1V^GPAW>MZg>MLlJcz^Y&Nx{A3@{sao|GS)29nzS;F!w`SM&YA$s-AzqkM< za4i6XzrESIXZ#?Tqj*)g&zpylzQ+~Q=$NJ2cG!oM?Mb~X=UjRvcB)3s%-nn{os9{> z?9PzeL1wqT&riKHJF{nrucE2Q{zp&s{E3PkOYtZGYji7{8!Ky`f1BKyE@qMt<^kOa z@g^OvznR&#aU-9rcsBKwe_x+;u*RDT=B8(ppm{>UDgnE`DTBe+mQJd|z@`qGg24vTE(x8^F$01L%rcANyu`b%S81} zjELixCnW}BRJnqbu)F1m_DE{iV*|}9oqIhEc~tjA={Ab!;{Zm5ShCqvK=MImpmb>n zr7yw4^Y3CFL*qDL4PhNWH7^bd*9a-NR{iyRpga;#dDJv8q8fMn=)~>jFgm z!X_2RM11YT2R0i%gZ{SsyQgqA23-)igW7SEWYH<-tZa#-b#rzZSf-=n4%`uJqozJH zX55%_o)gLve?EroL4UtZp=)6g;}qGa+dbwdaL~l0L~N?3r{^|mJ|xV*c9w%udbt^IXLbX8{sNk8BIIOo_sSNrK&xJSG5@~5?EwIt2Qk!2eXP&LDYmlIja{xsX ziV11SnLU?++eICS>Vt4L1gTd{GXZH#z(Ta5R%N3}}lU zBZ3TKyg#9?0i7V9LA5LF?9?3pL%VK@Z;xLdN-$hCFsN(CRM}8~c?B$kF3@OXX**jd zNg70k;~YeEqVhp8mX@BrMzFW0W?{vp0JItqw-6Cm;OBgZ(F{W6wGtIXfK@^FBFmOF zW*NC5<>x(=o^5|C#V6`kqwVwf;e*T*^ggSakDneCTxR5{xecx)OwWpnv|$K1od*Ug zI79JVFUNbN{M!t`FvXCPu*ksypBf6C)|wgK9aMofOvZ2l!4c|Rk2mAs0DwjH*c0dk zaX}KbAnLcZsiA_7Dq~%#lH~oSO9}CLQW6n+ED;GF=?}MGzsfWsqxTL?wuQ!firT}w z&&B1%{WEbvbN%0jC@Wli;DCwX8%|t6f&5d3LmAEa zuq1ZwEL{qc)k7c`DVJ^{iaSn+0!C2GT!cf8^6fcJ5zYG#6yXnT|cK>EnTYp<_Wfeh8qDIGkR~I^WV#yLwRnxxIyT>h7Uc_l*Z~`I(q1m^Ynqarw5fX^5~sH8+o#WM`Nb zJbbuA?i2%hCXa84R{FlSwhOT;M#VXBZYwD$1b|VepjTHeSeD=7MClztQ@#050n$-l z-{$9fG3W)z8sY1trGZnP@U|62)N3=e|2?h=pdZwljvkIr|i8lzc1O4dLT9cy< zSj{Cf*;Lwf(3hjjPIJTv6p60vqrd{;MT=HF&rBxRMw%R0>Z@MJLt{vwTPgC7E904D zGyXli2i}Poj1Mg23Ldll2D|d*!opkj8C0=d0>No{Xh(2+^b7u}hKE6Q{;A1{iEVQQ z39JW$6L>PvEU7bg11flS_(L}ebiwGr<~0=XJCqWAzXe8FILN|c_C6YNz&T+((vMS9 zXxEYem8f0HzTJIo_f_xPWvhL2x*V&eo)SdO1HBp(@6mH2%C67_LX54W`n&Y%MxcZw zb4Oe%bD0B22G#vchFNF2f*9TS`+6CfA1n4HX-cbwSsLVa8~2SB^YHMLb~tBuP=Q7f z0HLTe9tWvsMzZm&+WuUv-a`3~No^y6mDJV3c+TeC9G1>1-+bKU1dgLk&Kd3YyT`E3 zS=%q()o0ZCRcEq%RbiEjkDhcB-qndiGd`&qmA2W+&vG-~Zxk```-=X}*=KoEfh`z0 zBbh7l+J*~GUn*l>ZZERmL=HA8iaQ78BG~Z>as&GWk}qDo7-rY|TGsBtg&4|;D`U%j zJk3-mkHY|?YwSC*7mhwT=_uHTT{L|8ncXEC@ zlz!Id%_*e}TIek6H&u!Cd~oD6=SWh~L8eR!wMa15#~Y&&tW-VuW;g|5j0^a^JJ zUw6MuKHVUWL(}KyHAg}1D}zeD8$cAUrr)!BVo_ZmA6s{@BJ)#-ub1NJY#<94JWRW{}ucr_8bhgzdU05wS;Bh$-1;+CyAJ50c z#^C>bwrts6$MiM~&^c)N)%4z#>cZIvpBu-wxJVP}W-I>yxnkMm@Q^8Awp?-2n`2P7 z#LcDCbrn*gJUuu+=)|USy7W4`R?1y*b@FW4-kiy%A(Z;QMcqBGdi+H0YtLiT4ZpH@ z6Nd{<-4~1h7L@B`XSRef<-nFNlyo_J@PKNvCBKZBza1(0`}uJX98KZmT~HBv>fAs>C-+U{-|B(bgMVlyD82q1Xb7e_GCD7N=}6YYiJtK3nP5Pb+;NFl z9llvRj~x>y5@--x0qJ!I9NZOEtC8Fa$ga)#J;J2TFwVjFD9*DVQH_?_6tEyOomy7i zaLB5&Pe@Fx53Bjs&;oBOIOjjH)ERQ>8-GEy#WzKc%0t`wyVM;CL*Iy~DvR6;0sW%0 zL;OuWY%yBB@fhdB#H;qT$GOH#mIx#2ILCNkji>MwqCBQ3%qiY!+p`u>RxL9g+f5ri(gqxo&mmvV^aFcX1C<9Lk~l)#P7OcuY2m{lP7 z_(TGVFaUiZ9Hn6!!K(2cZcQ@ZS(Lxm@#BMQi-HV*Q$>Z5X6tKQaWGjFiZTY3UThv5 z#*?3?s$jqh9JlDl7El3rzyWRwtD*KLrDE{PCiXsuEdmV&3u0d}au#WXe5B>~)qTvG znVYFSP*IQk_{a~IJSdi?qPurpR5>j=WxZ?Xxf8K4puT8rE%NBSs>inwHd>B5QlM%mUNvybt>?+4@%*dB)-JQ zkLQEd29i1vev2p^E-3~GZrC!>ZQCozhKY%HSLmh_DJRu|`hwM(j}8SGVoR04)&q16 zvCq1@YioV)BIp3cDTaN=8ZsS>CjED}JVqQg$k}A?g8ck^g&;#!&Wc{JV&%&Bjg9wXL=+wB$qa_C_wZYG`GH&=;>ar7gqQZoo|RzTLjS2W zRT<4#V*uCb(&1?4bTo(>Z`@jZP#d{oX~w-q7?umQ{N1~YZTXlsEzmpnO-H#OZlWka zFab5Mk;R)jXyp@t^#nEn-tID>6$tkr_^d4S9-I)u;PEV)2_jn$Sx2BJg7Z%pH$tw0 z$Y~&hFoT9(7fbEOm;~s@D}d`l&_Rd>FxF3qmrTA%P=dSh=tnOAYES14;H%B|$yI$N zNFqj07#O|5C}>NGAAhxh15sldrZc#X5*Wn%UWPJ!ieQU(^`*|Up5RNC35 zi_jQ#TK^WJExpoE%!$sQ znSgP_QCRW+z4H**vIx8QMN5irlcAW&qnR{ivghcJg_shH`$kqb7_ep8VyA+C@${)~ z^DRU=>zI9@9U|W6n9m!&UT*m@|~8%x?5uqD3N^R1`cpI zw+9G(iFkp_95V#)Qi%v2_$b=fT|^rS7t@tTGVR|ISx~ocOB)OVYz+@<6@Bk%C_ z#%s*1V+^z~zC7nh&sVPkBNcM&kuD9U5OY+Xc@cJFg|5gQPOq?6yr=5VOe&zR_wd5s zQ+uN$BdXC$H7YD_0~nm3gQLW(B!>*IM`DhQ*3?<>2g_Z+c# zs7<-b48S;%cywUzzI_oeti1bdFiE)10PIf4Bd9VH=^x?J8T+*gBNj#wYn;wJoK=@5slOR(MKA7zP^@$0#s## zb?er_IVw!7>f1MzQA^4p+5x{sh{R$b5f|FS$wjA88xg{<-I!osJ);54QxF#eQI=rS zuXDm#2=C;qwzMQcZ=MW3H$Wu-^(Eu`2byw~5(myHKfoVWhqx6@*U8(fR>~1;4`HD$ zn|hu9upoeyuLp4Pljkw(4|sB9K!8!$hqNuTL0+G#o3YY62ZU_(60Qx#d(Oezc=c2L zc{pq9dn#KVW%5i~Y;G{%UlJ8$UNNCf)Q25CwGM{qqi-jPFstjKG=lQ;&An*9RfXNj-4^ zQR+Uy7*9M*PZvP;lqPt8?`L4pF*m+aXiZgYwSQnx&{+oEgWz%)?ZRO*>g%)O2#O@I zKb|`@WF}}P0O?2k)5mot(LNJGp&}}Q?d``f8q(zO|N8ao<%USGNMYj~)^;NWw>!ZK z_;Tuoak6+uNWS8CL};aC}s2bFdckRxz zsr$G%f(U(r2ww>%vuVgzaO^d(&i8q7m3>L5o*^M5rGL#=D@k@6ngc^`72L#Ot7>>Oid+*wy!kJ)SLU?vw5WPm54K zAoIwwVhA%^Ael(qz$hK({1ya&iN`1RRB~AzuxUVPeox8iUEeq;o;-jZ6#Bv2{`?!cM=j%CmnV(cLrxRci0oP<{xDU4maHeEsWd(6ZyHb@kJ&(p| z$eZWlx_F2QrJQ|%a(TH1MPIbd74#p%n#S6CCnHOJ!(q`n1r?R`HF4jvUI9y#Q`JEF zh0bud^`UIOGZM6?&DE$B)6<_l<<+9f_4FAo6uY=ftw-W3nGa@j;aNcqqIfHWG10Vo zTmIftG+Q95IAbrpOMRoxHxO}v*ezxnMAvoaFn@5j=?g5jus9XVTNzPdc%=Emi=5l+ zn_o`{7>_6hHw@h3v zv~u75IT@?Ym*l`Db$4ig`V2P8%5BbbP|x7lFe+)uLbswXDuquFy!fxakLv4*L`Y}M z9Whh;;B%zC6J;i0mxn{Vq%{$MoBqeElbM8u-$#@vsYAp-VK=hDNssi?tF#M5vF&5^ z_wTTJ+Pp>oRojW|9$WRNePJ&VY?85v2~x1&DN zL`w|0I7;qD71_TYL)7f_WY^YA>;QLFX{ge(PM$ukS-Ewqf&!+UJCN~l_lX#&+k+yv z{_5ckW*dEp6%9SE?*=X)HWWB)Oax{P$T`EfO*4=P&_OE+06xU)G)t0(zON>d_4GD3 zl%wauUk2;$uqTgrSj^5Y!FrxSxs1ZfgO zAePle8Y%|#GelK*fm4LCT9jN z$q>lwr|nX{1IgNF_Ut!F_2?pP0&`_*Mv1$&^0`QG`%oduGCmRz88D6t&k-FQYKzmS zH$gL+m?(qKe(wvQ89>Th6oQF@=n*oX^EQ%`pXy=RMZOt^AvaG7GJg6>KD%>EL5;b4 zZ)W3Uh{Q>|MRSNs`ts%2S+ovgc}z6{L=h3tzP{>MWH3E$+k{F7hQ;`GG!twqWQE`v z5!tqYX3h>h8#`6J-o_WAq@e_1wgv(#zH#H?b=PaEM1laV9 zjE`NQ?}*KVFBK{O6dK5Ghq6yzCpD#;Xes;X9}WijdH*yw4%=FFk8<TC0lMQnf3!7{;U=N|bOGrnHL%ZJI zN=yXG46g@vo|QXr>p|ETE4|INn=dCfx1y=(8WDgp86rT+z}T}s!w$5ct1GwPM*1wl z0|MSSMgLVs7?`8No4!wsn2-jNn=6crJQ6??Fj@)TG^7E9XBQ&kkn$00y(Y4<=RHWs zcsyLG@kuU^J{^^-pQxDhOOGpK{q&csOIdiUgVu4C2l`;h9+G9)D#Muu6Jyx^VKx}Z zR|?xdR65Z8<5aZP3FOmGGJV<3_)`QwCfCIB!o@?8op=I5^lrmJPOEc2%oqEv@6}u& zc$ZpU@Crmw}I*qF~TL;C#5`Z~glNKd|T)c)Jx49zA;W=pmnsg2Ga{$h|3? zOSgFjms~CIG8Yj*2eRm^Zw^0xa)E+-!7Gy_6Blo9xoX;4?6#iN9EmY28ZT79m_s99 zri2D;4eDU@**GA+RYuE4MBTcT`2@{Hy%G%MK#72?5SohmdKt|G_(JS362J-su$Ey4 zjSr$ZWMt?Keki~+IWhp965IfQim+J4^MmC{_PH(|z%F5k9fr5NdtP%`M6mh&%r-xRw7}Ol4dg{I}?v@BgKiz3MfDHrxgYEOrCyP~~ zV86KQz(TR|iV87J>6u<{EM7hm0Ui--{=0Xtr@mAsM&Ll90PNfhd3^fB2>?Oj-FYm( zd%wYS?fmOjdOZh-Ap;gxn{R6XGmBt0wnw=D+jLE8Q>($Be{@B3|6pdok^(363ygfzMR0@{Iw)ZOc6rokB^ z=Jfz?9!fJRO#CZH@Pr}MLqEg<|6;I8i3JGPs)v)DRes{4eso1D=OS}uhFp9H>$@ zfaCnn%YKWjjL!$QP2l7~5|4r0J~?uj-KKSWT*XYV)%WbV8ux6Bujh?F8`ufqsl-wO z+JM9GEf^T+{BXQ3)=WUthn1)q#M6e_0GO4d<6wZ833_64R~S?B0Im&O&s-Pm=h>n0 zrKVk~BSv14FF1d#N$Aa%bFBU0x&3eM3Qp*|PlU)T+7_dEgcSKzlVi$4ZQ-8{UN>Dv z&YrG2;6V#tChW`98`6SJR{(gI05U-w35mc9^CT_4~XHh74 zqY#REw8umUVOiIWgQHM!Z;^FpAn8L`z3>5N5tdC%inm=AZL~jLWh5xNsyNS&Wxrl= z@6+snDefF>xK>n$i+e78C6m>rl&?&8`FbUaP37XU_EqQ_A^iQA*)3LK7R8?}O*O;m zf<0E~;E?;Wl~(Ha_-%}6vIDh$`nU)FDr?jVm_M$VyfEu;@cS|+3USYyorgAL*EkiY zE;}=lzkbVL13&MP%Im|$PQt@q4$U{q$Ov+*-p85mb!dam)8vz8YgkLjXzM@qzmLWt z*+p#9v#(^`KmUEUv&}PE(-&-XI5(mF?`S@J z<(M^bZF*;kr!+Vty?u7vMyPKs? zRS|QMlrSrQ!Q~QYI95Mh5o2Bz;aF60Ksc&drtsbF?a%Hk!%wGrWb-asaN4&0fipv= zKX%#c?R_`%hf03lLH}f1-P*gi)(rLac?;@&7aC=H@eaNz&ZU<%p{QiP_p&wR;~RYG z`-7b}iC=e&!`S%#ttO6MZJE4WTp(u0;()@5u(>f3qdJM>x8*m68buLWPE>yP`XgS5TW_7x{XoJVp~m2$D?;Q3#Cm#7CFf z`oS*(4{eo)D-Z6exug^foDO{4hducg#MyEvx~&%u8+y2#CQ+yn3}uVmtIzv(?lZ@Nxhg zpgcpV0nsWJp&85)p7e74IJ99inHo#8KxF|&duUa}8riE69a*ufpiL}3^OkOPv(0ax ziFomuouS_^RtfkmU!uoxK5dJEy5dH%+46PZ7S`r|3Pw-j^6ZwNBNIG3B1{nO>JY|$ zY%nz~LiLGR?5vW*dTaQnPBUYU?-dnr&cPxuWBrOrcKY^lx zwcjy_^Bw}owt^%LuQ#!I#6SsMo~rj0pQ))SW;!si2g(^(7UN`qily-Kmf7cVZuWWn zOH3dR*egqIIuBN(==CrGwXV4%=DBoctWQR036i9AMAeqNkXMd8Y7uqROZYBU#k{>S z&=AXk%Tr#BTyOh}T>+bHGgrvZE$9Y5RcK)@HaY=<;`5!AK=C2o6}LHYlR-y0ipao^ z#XRuUfb29e5t|Mv%_THUvx#QPNC3=v6c24cVc%m7RRDJ%&12D|t!7Wx&Mkj2=YH2+ zCzJnU-319*APs(KpE?)4a^7B%)EjSgu&w@SYwK8b9Z87muxzk^I}?@TJhdUevg+2K zry1i_{Xh0U%N{!P43k*iv$IhNL3N%EG38og|#=cfl8arBS z-i3ksrDK+?8*S|8-#-d!Ej8z z<^=Ar>%R*)z@O#aMc8QI(LY4CI}ohkcB}RJ9RB~F^%^k@wyyMY5fOL>3J|(}WfLp! z5$*SUGDzwLW(`H(sujY*P&YZlzr9fUOSHz*4>)l;#YcbtaI-9#S+XOFVMAkn)zS`a zF%w5e$3~U)6=tAfg3i?=p)x{qqH`W)o{3pDIXC6QPb;a!efpKT)RxqW_U2G4FT0WW z0^2un>v_DrQs-uUrRwYo;Eb8rO6G|f>+xvoZ^$C*dI*HzBQFGkI+wpO8Xgv1vbPTq z@TU3ANLN}tq=JU_^-SyAODUxTw*gqO?R9;0O7J9)$5A_N>&YA;E3p>sBNtu2`nsa(R*()1a{=+4;1q@iJDQ z(LMfeNJo~LIF;xvPQ9DllC};ukXVmbxNNqJWbv;^7Sq+)PHe1-EN><2D*m5WK(lq- z;{OMr_J0*DyYoF$g)^RK=>6NDvUrX~K3=n(q;%$A-N80=b^teVFXLa>3+$v|GEYK< z)_)=QbuzfL;F&G}!W-fh^th(xX`#vpK{F!?;PI1Qjae+ewab@}&DQ0p-+;%Xup_!! zsK5cG;@86Chz!ELi(fk(@a-Tt4geem>u;8jV{-hY3d1&Eaj=1vh@+*IHrs(I#-^E? z27r%+B)``iaBSeX1#%!wUbIq3JKYH7iu;9J+5h5CJ=&B(||fOM-RBs7~3SXf-c&9_EU za(;jzs9F%F0PO+__7~6Lq8n4 J6w=X){|DY{%{Tx6 diff --git a/source/incoming/controllers.rst b/source/incoming/controllers.rst index 18475b41..067f796c 100755 --- a/source/incoming/controllers.rst +++ b/source/incoming/controllers.rst @@ -152,7 +152,7 @@ $this->validate() 自 v4.2.0 起,引入了新的更安全的自动路由。 -.. note:: 如果你熟悉自动路由,它在 CodeIgniter 3 到 4.1.x 中默认启用,你可以在 +.. note:: 如果你熟悉自动路由,它在 CodeIgniter 3.x 到 4.1.x 中默认启用,你可以在 :ref:`ChangeLog v4.2.0 ` 中看到差异。 本节描述了新自动路由的功能。 @@ -180,7 +180,9 @@ BaseController 为加载组件和执行所有控制器需要的函数提供了 然后将该文件保存到你的 **app/Controllers** 目录中。 -.. important:: 该文件必须命名为 **Helloworld.php**,H 字母大写。当你使用自动路由时,控制器类名称必须以大写字母开头,并且只有第一个字符可以大写。 +.. important:: 该文件必须命名为 **Helloworld.php**,``H`` 字母大写。当你使用自动路由时,控制器类名称必须以大写字母开头,并且只有第一个字母可以大写。 + + 自 v4.5.0 版本起,如果你启用 ``$translateUriToCamelCase`` 选项,则可以使用驼峰命名法的类名。详细信息请参见 :ref:`controller-translate-uri-to-camelcase`。 .. important:: 通过自动路由(改进版)执行的控制器方法需要 HTTP 动词(``get``、``post``、``put`` 等)前缀,如 ``getIndex()``、``postCreate()``。 @@ -204,6 +206,8 @@ BaseController 为加载组件和执行所有控制器需要的函数提供了 .. literalinclude:: controllers/011.php +.. note:: 自 v4.5.0 版本起,如果你启用 ``$translateUriToCamelCase`` 选项,则可以使用驼峰命名法的类名。详细信息请参见 :ref:`controller-translate-uri-to-camelcase`。 + 此外,始终确保你的控制器扩展父控制器类,以便它可以继承其所有方法。 .. note:: @@ -334,6 +338,8 @@ URI 的第二段通常确定控制器中的哪个方法被调用。 .. important:: 目录名称必须以大写字母开头,并且只有第一个字符可以大写。 + 自 v4.5.0 版本起,如果你启用 ``$translateUriToCamelCase`` 选项,则可以使用驼峰命名法的类名。详细信息请参见 :ref:`controller-translate-uri-to-camelcase`。 + 使用此功能时,URI 的第一段必须指定目录。例如,假设你有一个位于这里的控制器:: app/Controllers/Products/Shoes.php @@ -349,6 +355,28 @@ URI 的第二段通常确定控制器中的哪个方法被调用。 CodeIgniter 还允许你使用其 :ref:`定义的路由 ` 映射 URI。 +.. _controller-translate-uri-to-camelcase: + +将 URI 转换为驼峰命名法 +========================== + +.. versionadded:: 4.5.0 + +自 v4.5.0 版本起,已实现 ``$translateUriToCamelCase`` 选项,该选项与当前 CodeIgniter 的编码标准很好地配合使用。 + +此选项使你能够在控制器和方法 URI 段中,自动将带有连字符(``-``)的 URI 翻译为驼峰命名法。 + +例如,URI ``sub-dir/hello-controller/some-method`` 将会执行 ``SubDir\HelloController::getSomeMethod()`` 方法。 + +.. note:: 当启用此选项时,``$translateURIDashes`` 选项将被忽略。 + +启用将 URI 转换为驼峰命名法 +--------------------------------- + +要启用它,你需要在 **app/Config/Routing.php** 中将 ``$translateUriToCamelCase`` 选项设置为 ``true``:: + + public bool $translateUriToCamelCase = true; + .. _controller-auto-routing-legacy: 自动路由(传统) @@ -364,7 +392,9 @@ CodeIgniter 还允许你使用其 :ref:`定义的路由 ` 自动路由(传统)。很容易创建漏洞应用,其中控制器过滤器 或 CSRF 保护被绕过。 -.. important:: 自动路由(传统)会将任何 HTTP 方法的 HTTP 请求路由到控制器方法。 +.. important:: 自动路由(传统)可以使用 **任何** HTTP 方法将 HTTP 请求路由到控制器方法。 + +.. important:: 自 v4.5.0 版本起,如果自动路由(传统)找不到控制器,它会在控制器过滤器执行之前抛出 ``PageNotFoundException`` 异常。 考虑这个 URI:: diff --git a/source/incoming/controllers/015.php b/source/incoming/controllers/015.php deleted file mode 100644 index 2de1b716..00000000 --- a/source/incoming/controllers/015.php +++ /dev/null @@ -1,3 +0,0 @@ -setDefaultController('Helloworld'); diff --git a/source/incoming/filters.rst b/source/incoming/filters.rst index 59677a20..6d7c0594 100644 --- a/source/incoming/filters.rst +++ b/source/incoming/filters.rst @@ -4,7 +4,7 @@ .. contents:: :local: - :depth: 2 + :depth: 3 控制器过滤器允许你在控制器执行之前或之后执行操作。与 :doc:`事件 <../extending/events>` 不同,你可以选择将过滤器应用于特定的 URI 或路由。前置过滤器可以修改请求,而后置过滤器可以对响应进行操作甚至修改,从而提供了很大的灵活性和功能。 @@ -66,16 +66,17 @@ 如果你想为特定的路由指定过滤器,请使用 **app/Config/Routes.php** 并参考 :ref:`URI Routing `。 -在路由中指定的过滤器(在 **app/Config/Routes.php** 中)会在 **app/Config/Filters.php** 中指定的过滤器之前执行。 - .. note:: 最安全的应用过滤器方法是 :ref:`禁用自动路由 `,并 :ref:`设置过滤器到路由 `。 +app/Config/Filters.php +====================== + **app/Config/Filters.php** 文件包含四个属性,允许你精确配置过滤器的运行时机。 .. warning:: 建议你在过滤器设置中的 URI 末尾始终添加 ``*``。因为控制器方法可能比你想象的通过不同的 URL 访问。例如,当启用 :ref:`auto-routing-legacy` 时,如果你有 ``Blog::index``,它可以通过 ``blog``、``blog/index`` 和 ``blog/index/1`` 等方式访问。 $aliases -======== +-------- ``$aliases`` 数组用于将简单名称与一个或多个完全限定的类名相关联,这些类名是要运行的过滤器: @@ -89,10 +90,27 @@ $aliases 你应该根据需要定义尽可能多的别名。 +.. _filters-required: + +$required +--------- + +.. versionadded:: 4.5.0 + +本章节允许你定义 **Required Filters** (必需过滤器)。它们是应用于框架所做的每个请求的特殊过滤器。它们在其他种类的过滤器之前和之后应用,这些过滤器将在下面解释。 + +.. note:: Required Filters 总是会执行,即使路由不存在。 + +你应该注意在这里使用的数量,因为在每个请求上运行太多可能会带来性能影响。但默认设置的过滤器提供了框架功能。如果移除,这些功能将不再工作。详细信息请参见 :ref:`provided-filters`。 + +过滤器可以通过将它们的别名添加到 ``before`` 或 ``after`` 数组中来指定: + +.. literalinclude:: filters/013.php + $globals -======== +-------- -第二部分允许你定义任何应用于框架的每个有效请求的过滤器。 +本章节允许你定义任何应用于框架的每个有效请求的过滤器。 在这里使用太多可能会对性能产生影响,所以要小心。 @@ -100,15 +118,17 @@ $globals .. literalinclude:: filters/005.php -除了少数 URI ---------------------- +排除少数 URI +^^^^^^^^^^^^^^^^^^^^^ -有时你希望将过滤器应用于几乎所有请求,但有一些应该不受影响。一个常见的示例是,如果你需要从 CSRF 保护过滤器中排除几个 URI,以允许第三方网站的请求访问一个或两个特定的 URI,同时保持其余 URI 受保护。 +有时候你想将过滤器应用于几乎每个请求,但有一些请求需要被排除在外。一个常见的例子就是,如果你需要从 CSRF 保护过滤器中排除几个 URI,以允许第三方网站的请求访问一个或两个特定的 URI,同时保持其余 URI 的保护。 -要做到这一点,请在别名旁边添加一个包含 ``except`` 键和要匹配的 URI 路径(相对于 BaseURL)值的数组: +要做到这一点,请在别名旁边添加一个包含 ``except`` 键和要匹配的 URI 路径(相对于 BaseURL)值的数组: .. literalinclude:: filters/006.php +.. warning:: 在 v4.4.7 之前,由于一个漏洞,被过滤器处理的 URI 路径没有进行 URL 解码。换句话说,路由中指定的 URI 路径和过滤器中指定的 URI 路径可能会不同。详细信息请参见 :ref:`upgrade-447-filter-paths`。 + 在过滤器设置中可以使用 URI 路径(相对于 BaseURL)的任何位置,你都可以使用正则表达式,或者像在这个例子中使用星号 (``*``) 作为通配符,匹配之后的所有字符。在这个例子中,任何以 ``api/`` 开头的 URI 路径都将被免于 CSRF 保护,但网站的表单将全部受保护。 如果你需要指定多个 URI,可以使用 URI 路径模式数组: @@ -116,7 +136,7 @@ $globals .. literalinclude:: filters/007.php $methods -======== +-------- .. warning:: 如果使用 ``$methods`` 过滤器,你应该 :ref:`禁用自动路由(传统) `,因为 :ref:`auto-routing-legacy` 允许任何 HTTP 方法访问控制器。以你不期望的方法访问控制器可能会绕过过滤器。 @@ -129,16 +149,18 @@ $methods 除了标准的 HTTP 方法外,这也支持一个特殊情况:``cli``。``cli`` 方法将应用于所有从命令行运行的请求。 $filters -======== +-------- 该属性是一个过滤器别名数组。对于每个别名,你可以为 ``before`` 和 ``after`` 数组指定过滤器应该应用到的一系列 URI 路径(相对于 BaseURL)模式: .. literalinclude:: filters/009.php +.. warning:: 在 v4.4.7 之前,由于一个漏洞,被过滤器处理的 URI 路径没有进行 URL 解码。换句话说,路由中指定的 URI 路径和过滤器中指定的 URI 路径可能会不同。详细信息请参见 :ref:`upgrade-447-filter-paths`。 + .. _filters-filters-filter-arguments: 过滤器参数 ----------------- +^^^^^^^^^^^^^^^^ .. versionadded:: 4.4.0 @@ -148,6 +170,22 @@ $filters 在这个例子中,当 URI 匹配 ``admin/*'`` 时,数组 ``['admin', 'superadmin']`` 将作为 ``$arguments`` 传递给 ``group`` 过滤器的 ``before()`` 方法。当 URI 匹配 ``admin/users/*'`` 时,数组 ``['users.manage']`` 将作为 ``$arguments`` 传递给 ``permission`` 过滤器的 ``before()`` 方法。 +.. _filter-execution-order: + +过滤器执行顺序 +================ + +.. important:: 从 v4.5.0 开始,过滤器的执行顺序发生了变化。如果你希望保持与之前版本相同的执行顺序,你必须将 ``Config\Feature::$oldFilterOrder`` 设置为 ``true``。 + +过滤器按照以下顺序执行: + +- **前置过滤器**: required → globals → methods → filters → route +- **后置过滤器**: route → filters → globals → required + +.. note:: *required* 过滤器可以从 v4.5.0 开始使用。 + +.. note:: 在 v4.5.0 之前,指定给路由(在 **app/Config/Routes.php** 中)的过滤器会先于在 **app/Config/Filters.php** 中指定的过滤器执行。而在 Route 过滤器和 Filters 过滤器的后置过滤器执行顺序并没有倒序。详细信息请参见 :ref:`升级指南 `。 + ****************** 确认过滤器 ****************** @@ -181,14 +219,56 @@ filter:check 但是当你在路由中使用正则表达式时,它可能无法显示准确的过滤器。 具体详情请查看 :ref:`URI 路由 `。 +.. _provided-filters: + **************** -提供的过滤器 +自带的过滤器 **************** -CodeIgniter4 提供的过滤器有: :doc:`Honeypot <../libraries/honeypot>`、:ref:`CSRF `、``InvalidChars``、``SecureHeaders`` 和 :ref:`DebugToolbar `。 +CodeIgniter4 自带的过滤器有: + +- ``cors`` => :doc:`../libraries/cors` +- ``csrf`` => :ref:`CSRF ` +- ``toolbar`` => :ref:`DebugToolbar ` +- ``honeypot`` => :doc:`Honeypot <../libraries/honeypot>` +- ``invalidchars`` => :ref:`invalidchars` +- ``secureheaders`` => :ref:`secureheaders` +- ``forcehttps`` => :ref:`forcehttps` +- ``pagecache`` => :doc:`PageCache <../general/caching>` +- ``performance`` => :ref:`performancemetrics` .. note:: 过滤器按配置文件中定义的顺序执行。但是,如果启用, ``DebugToolbar`` 总是最后执行,因为它应该能够捕获其他过滤器中发生的所有事情。 +.. _forcehttps: + +ForceHTTPS +========== + +.. versionadded:: 4.5.0 + +此过滤器提供了“强制全局安全请求”功能。 + +如果你将 ``Config\App:$forceGlobalSecureRequests`` 设置为 true,这将强制所有对该应用程序的请求通过安全连接(HTTPS)进行。如果传入的请求不安全,用户将被重定向到页面的安全版本,并且会设置 HTTP 严格传输安全 (HSTS) 头。 + +.. _performancemetrics: + +PerformanceMetrics +================== + +.. versionadded:: 4.5.0 + +此过滤器提供性能指标的伪变量。 + +如果你想显示从 CodeIgniter 启动到最终输出发送到浏览器前这一时间段的总耗时,只需在一个视图文件中放置这个伪变量:: + + {elapsed_time} + +如果你想在视图文件中显示你的内存使用量,使用此伪变量:: + + {memory_usage} + +如果你不需要此功能,请从 ``$required['after']`` 中移除 ``'performance'``。 + .. _invalidchars: InvalidChars diff --git a/source/incoming/filters/008.php b/source/incoming/filters/008.php index 8d417fd4..7538a199 100644 --- a/source/incoming/filters/008.php +++ b/source/incoming/filters/008.php @@ -9,8 +9,8 @@ class Filters extends BaseConfig // ... public array $methods = [ - 'post' => ['invalidchars', 'csrf'], - 'get' => ['csrf'], + 'POST' => ['invalidchars', 'csrf'], + 'GET' => ['csrf'], ]; // ... diff --git a/source/incoming/filters/013.php b/source/incoming/filters/013.php new file mode 100644 index 00000000..5e7154bd --- /dev/null +++ b/source/incoming/filters/013.php @@ -0,0 +1,23 @@ + [ + 'forcehttps', // Force Global Secure Requests + 'pagecache', // Web Page Caching + ], + 'after' => [ + 'pagecache', // Web Page Caching + 'performance', // Performance Metrics + 'toolbar', // Debug Toolbar + ], + ]; + + // ... +} diff --git a/source/incoming/incomingrequest.rst b/source/incoming/incomingrequest.rst index bdaabe07..b051a524 100755 --- a/source/incoming/incomingrequest.rst +++ b/source/incoming/incomingrequest.rst @@ -37,7 +37,9 @@ is() .. versionadded:: 4.3.0 -自 v4.3.0 起,你可以使用 ``is()`` 方法。它返回布尔值。 +自 v4.3.0 起,你可以使用 ``is()`` 方法。它接受一个 HTTP 方法、``'ajax'`` 或 ``'json'``,并返回布尔值。 + +.. note:: HTTP 方法应该区分大小写,但参数是不区分大小写的。 .. literalinclude:: incomingrequest/040.php @@ -48,14 +50,14 @@ getMethod() .. literalinclude:: incomingrequest/005.php -默认情况下,该方法以小写字符串形式返回(即 ``'get'``、``'post'`` 等)。 +HTTP 方法是区分大小写的,按照惯例,标准化方法是用全大写的 US-ASCII 字母定义的。 -.. important:: 将返回值转换为小写的功能已被弃用。它将在未来版本中删除,此方法将等效于 PSR-7。 +.. note:: 在 v4.5.0 之前,默认情况下,该方法会返回小写字符串(即 ``'get'``、``'post'`` 等)。但这是一个 bug。 -你可以通过将调用包装在 ``strtoupper()`` 中获取大写版本:: +你可以通过用 ``strtolower()`` 包装调用来获取小写版本的字符串:: - // 返回 'GET' - $method = strtoupper($request->getMethod()); + // 返回 'get' + $method = strtolower($request->getMethod()); 你还可以使用 ``isSecure()`` 方法检查请求是否通过 HTTPS 连接发出: diff --git a/source/incoming/incomingrequest/005.php b/source/incoming/incomingrequest/005.php index 3c93e74b..37e6f490 100644 --- a/source/incoming/incomingrequest/005.php +++ b/source/incoming/incomingrequest/005.php @@ -1,4 +1,4 @@ getMethod(); diff --git a/source/incoming/incomingrequest/027.php b/source/incoming/incomingrequest/027.php deleted file mode 100644 index aa4f0178..00000000 --- a/source/incoming/incomingrequest/027.php +++ /dev/null @@ -1,3 +0,0 @@ -getVar('some_data'); diff --git a/source/incoming/incomingrequest/028.php b/source/incoming/incomingrequest/028.php deleted file mode 100644 index 3bc49822..00000000 --- a/source/incoming/incomingrequest/028.php +++ /dev/null @@ -1,3 +0,0 @@ -getVar('some_data', FILTER_SANITIZE_FULL_SPECIAL_CHARS); diff --git a/source/incoming/incomingrequest/029.php b/source/incoming/incomingrequest/029.php deleted file mode 100644 index 62390522..00000000 --- a/source/incoming/incomingrequest/029.php +++ /dev/null @@ -1,4 +0,0 @@ -getVar(null, FILTER_SANITIZE_FULL_SPECIAL_CHARS); -// returns all POST items with string sanitation diff --git a/source/incoming/incomingrequest/030.php b/source/incoming/incomingrequest/030.php deleted file mode 100644 index 9e555a2f..00000000 --- a/source/incoming/incomingrequest/030.php +++ /dev/null @@ -1,3 +0,0 @@ -getVar(['field1', 'field2']); diff --git a/source/incoming/incomingrequest/031.php b/source/incoming/incomingrequest/031.php deleted file mode 100644 index 98c6f35e..00000000 --- a/source/incoming/incomingrequest/031.php +++ /dev/null @@ -1,3 +0,0 @@ -getVar(['field1', 'field2'], FILTER_SANITIZE_FULL_SPECIAL_CHARS); diff --git a/source/incoming/message.rst b/source/incoming/message.rst index c20a2ae6..c4ec7227 100644 --- a/source/incoming/message.rst +++ b/source/incoming/message.rst @@ -4,7 +4,7 @@ HTTP 消息 Message 类为 HTTP 消息中请求和响应共有的部分提供了一个接口,包括消息体、协议版本、用于处理头的实用程序以及处理内容协商的方法。 -此类是 :doc:`请求类 <../incoming/request>` 和 :doc:`响应类 <../outgoing/response>` 都扩展的父类。 +这个类是 :doc:`请求类 <../incoming/request>` 和 :doc:`响应类 <../outgoing/response>` 的父类,不能直接使用。 *************** 类参考 @@ -134,6 +134,19 @@ Message 类为 HTTP 消息中请求和响应共有的部分提供了一个接口 .. literalinclude:: message/009.php + .. php:method:: addHeader($name, $value) + + .. versionadded:: 4.5.0 + + :param string $name: 要添加的头名称。 + :param string $value: 头的值。 + :returns: 当前消息实例 + :rtype: CodeIgniter\\HTTP\\Message + + 添加具有相同名称的头(不是头的值)。仅在设置多个具有相同名称的头时使用。 + + .. literalinclude:: message/011.php + .. php:method:: getProtocolVersion() :returns: 当前的 HTTP 协议版本 diff --git a/source/incoming/message/011.php b/source/incoming/message/011.php new file mode 100644 index 00000000..b5b2c323 --- /dev/null +++ b/source/incoming/message/011.php @@ -0,0 +1,4 @@ +addHeader('Set-Cookie', 'logged_in=no; Path=/'); +$message->addHeader('Set-Cookie', 'sessid=123456; Path=/'); diff --git a/source/incoming/request.rst b/source/incoming/request.rst index 772b07c3..ce137466 100755 --- a/source/incoming/request.rst +++ b/source/incoming/request.rst @@ -47,15 +47,12 @@ 可选的第二个字符串参数为“ipv4”或“ipv6”来指定 IP 格式。默认检查这两种格式。 - .. php:method:: getMethod([$upper = false]) + .. php:method:: getMethod() - .. important:: ``$upper`` 参数的使用已被弃用。它将在未来版本中删除。 - - :param bool $upper: 是否以大写或小写返回请求方法名称 :returns: HTTP 请求方法 :rtype: string - 返回 ``$_SERVER['REQUEST_METHOD']``,可选择设置为大写或小写。 + 返回 ``$_SERVER['REQUEST_METHOD']``。 .. literalinclude:: request/003.php diff --git a/source/incoming/request/003.php b/source/incoming/request/003.php index 4b0b537b..d448962c 100644 --- a/source/incoming/request/003.php +++ b/source/incoming/request/003.php @@ -1,5 +1,3 @@ getMethod(true); // Outputs: POST -echo $request->getMethod(false); // Outputs: post -echo $request->getMethod(); // Outputs: post +echo $request->getMethod(); // Outputs: POST diff --git a/source/incoming/routing.rst b/source/incoming/routing.rst index e9632882..379fb0ec 100755 --- a/source/incoming/routing.rst +++ b/source/incoming/routing.rst @@ -46,8 +46,8 @@ RouteCollection 类 (``$routes``),允许你指定自己的路由标准。 这是一些基本的路由示例。 -包含单词 **journals** 的 URL 的第一个路径段将被映射到 ``\App\Controllers\Blogs`` 类, -以及默认方法,通常是 ``index()``: +URL 的第一个段包含 **journals** 的情况下,会被映射到 ``\App\Controllers\Blogs`` 类, +以及 :ref:`默认方法 `,通常是 ``index()``: .. literalinclude:: routing/006.php @@ -117,9 +117,7 @@ HTTP 动词路由 .. literalinclude:: routing/014.php :lines: 2- -如果忘记添加 ``use App\Controllers\Home;``,控制器类名将被解释为 -``Config\Home``,而不是 ``App\Controllers\Home``,因为 -**app/Config/Routes.php** 顶部有 ``namespace Config;``。 +如果你忘记添加 ``use App\Controllers\Home;``,控制器类名会被解释为 ``\Home``,而不是 ``App\Controllers\Home``。 .. note:: 当你使用数组可调用语法时,类名总是被解释为完全限定的类名。 所以 :ref:`routing-default-namespace` 和 :ref:`namespace 选项 ` @@ -193,7 +191,11 @@ HTTP 动词路由 将匹配 **product/123**、**product/123/456**、**product/123/456/789** 等等。 -在上面的例子中,如果 ``$1`` 占位符包含一个斜杠(``/``),当传递给 ``Catalog::productLookup()`` 时,它仍然会被分割成多个参数。 +默认情况下,在上面的例子中,如果 ``$1`` 占位符包含斜杠(``/``),在传递给 +``Catalog::productLookup()`` 时,它仍然会被拆分为多个参数。 + +.. note:: 自 v4.5.0 起,你可以通过配置选项更改此行为。 + 详情请参见 :ref:`multiple-uri-segments-as-one-parameter`。 控制器中的实现应考虑最大参数: @@ -240,7 +242,11 @@ HTTP 动词路由 .. literalinclude:: routing/019.php -在上面的例子中,如果 ``$1`` 占位符包含一个斜杠(``/``),当传递给 ``Auth::login()`` 时,它仍然会被分割成多个参数。 +默认情况下,在上面的例子中,如果 ``$1`` 占位符包含斜杠(``/``),在传递给 +``Auth::login()`` 时,它仍然会被拆分为多个参数。 + +.. note:: 自 v4.5.0 起,你可以通过配置选项更改此行为。 + 详情请参见 :ref:`multiple-uri-segments-as-one-parameter`。 对于那些不了解正则表达式并希望学习更多知识的人,`regular-expressions.info `_ 可能是一个不错的起点。 @@ -375,12 +381,18 @@ HTTP 动词路由 .. literalinclude:: routing/036.php -多个过滤器 +.. _multiple-filters: + +多重过滤器 ---------------- .. versionadded:: 4.1.5 -.. important:: *多个过滤器* 默认禁用。因为它破坏了向后兼容性。如果要使用它,需要进行配置。有关详细信息,请参阅 :ref:`upgrade-415-multiple-filters-for-a-route`。 +.. important:: 自 v4.5.0 起,*多重过滤器* 始终启用。 + 在 v4.5.0 之前,*多重过滤器* 默认是禁用的。 + 如果你想在 v4.5.0 之前的版本中使用,请参见 + :ref:`从 4.1.4 升级到 4.1.5 ` + 了解详情。 你可以为过滤器值指定一个数组: @@ -505,6 +517,8 @@ HTTP 动词路由 .. literalinclude:: routing/027.php +.. _routing-nesting-groups: + 嵌套分组 ============== @@ -514,7 +528,12 @@ HTTP 动词路由 这将处理在 **admin/users/list** 的 URL。 -.. note:: 传递给外部 ``group()`` 的选项(例如 ``namespace`` 和 ``filter``)不会与内部 ``group()`` 选项合并。 +**Filter** 选项传递给外部的 ``group()`` 时,会与内部的 ``group()`` 过滤器选项合并。 +上述代码对路由 ``admin`` 运行 ``myfilter:config``,对路由 ``admin/users/list`` 运行 ``myfilter:config`` 和 ``myfilter:region``。 + +任何传递给内部 `group()` 的其他重叠选项都会覆盖它们的值。 + +.. note:: 在 v4.5.0 之前,由于一个错误,传递给外部 ``group()`` 的选项不会与内部 ``group()`` 的选项合并。 .. _routing-priority: @@ -568,6 +587,30 @@ RoutesCollection 类提供了几个选项,可以影响所有路由,并且可 .. literalinclude:: routing/046.php +.. _routing-default-method: + +默认方法 +============== + +该设置在路由处理器只有控制器名称而没有方法名称列出时使用。默认值是 ``index``。 +:: + + // 在 app/Config/Routing.php 中 + public string $defaultMethod = 'index'; + +.. note:: ``$defaultMethod`` 也常用于自动路由。 + 请参见 :ref:`自动路由(改进版) ` + 或 :ref:`自动路由(传统版) `。 + +如果你定义了以下路由:: + + $routes->get('/', 'Home'); + +当路由匹配时,将执行 ``App\Controllers\Home`` 控制器的 ``index()`` 方法。 + +.. note:: 方法名称以 ``_`` 开头时不能用作默认方法。 + 但是,从 v4.5.0 开始,允许使用 ``__invoke`` 方法。 + 转换 URI 中的破折号 ==================== @@ -594,20 +637,21 @@ RoutesCollection 类提供了几个选项,可以影响所有路由,并且可 .. warning:: 如果你使用 :doc:`CSRF 保护 `,它不会保护 **GET** 请求。 如果 URI 可以通过 GET 方法访问,CSRF 保护将不起作用。 +.. _404-override: + 404 重写 ============ -当未找到与当前 URI 匹配的页面时,系统将显示一个泛型 404 视图。 -你可以通过指定 ``set404Override()`` 方法要发生的操作来更改此行为。 -值可以是与任何路由中所示的有效类/方法对,或者是一个闭包: +当找不到与当前 URI 匹配的页面时,系统将显示一个通用的 404 页面。通过在路由配置文件中使用 ``$override404`` 属性,你可以为 404 路由定义控制器类/方法。 .. literalinclude:: routing/051.php -在路由配置文件中使用 ``$override404`` 属性,你可以使用闭包函数。在路由文件中定义覆盖是限制在类或方法对上的。 +你还可以在路由配置文件中使用 ``set404Override()`` 方法指定在发生 404 错误时执行的操作。该值可以是一个有效的类/方法对,或者是一个闭包: -.. note:: ``set404Override()`` 方法不会将响应状态码更改为 ``404``。如果你不在设置的控制器中设置状态码, - 将返回默认状态码 ``200``。有关如何设置状态码的信息,请参阅 - :php:meth:`CodeIgniter\\HTTP\\Response::setStatusCode()`。 +.. literalinclude:: routing/069.php + +.. note:: 从 v4.5.0 开始,404 覆盖功能默认将响应状态代码设置为 ``404``。在之前的版本中,状态代码是 ``200``。 + 如果你想在控制器中更改状态代码,请参见 :php:meth:`CodeIgniter\\HTTP\\Response::setStatusCode()` 获取有关如何设置状态代码的信息。 按优先级处理路由 ============================ @@ -617,6 +661,24 @@ RoutesCollection 类提供了几个选项,可以影响所有路由,并且可 .. literalinclude:: routing/052.php +.. _multiple-uri-segments-as-one-parameter: + +将多个 URI 段作为一个参数 +====================================== + +.. versionadded:: 4.5.0 + +启用此选项时,匹配多个段的占位符,例如 ``(:any)``,将直接按原样传递给一个参数,即使它包含多个段。 + +.. literalinclude:: routing/070.php + +例如,以下路由: + +.. literalinclude:: routing/010.php + +将匹配 **product/123**、**product/123/456**、**product/123/456/789** 等等。 +如果 URI 是 **product/123/456**,``123/456`` 将被传递给 ``Catalog::productLookup()`` 方法的第一个参数。 + .. _auto-routing-improved: 自动路由(改进版) @@ -626,7 +688,7 @@ RoutesCollection 类提供了几个选项,可以影响所有路由,并且可 自 v4.2.0 起,引入了新的更安全的自动路由。 -.. note:: 如果你熟悉自动路由,在 CodeIgniter 3 到 4.1.x 中默认启用, +.. note:: 如果你熟悉自动路由,在 CodeIgniter 3.x 到 4.1.x 中默认启用, 你可以在 :ref:`ChangeLog v4.2.0 ` 中看到区别。 当未找到与 URI 匹配的定义路由时,如果启用了自动路由,系统将尝试将该 URI 与控制器和方法匹配。 @@ -698,6 +760,8 @@ URI 段 更多信息请参阅 :ref:`控制器中的自动路由(改进版) `。 +.. _routing-auto-routing-improved-default-method: + 默认方法 -------------- @@ -789,6 +853,8 @@ URI 段(传统) 更多信息请参阅 :ref:`控制器中的自动路由(传统) `。 +.. _routing-auto-routing-legacy-default-method: + 默认方法(传统) ----------------------- diff --git a/source/incoming/routing/004.php b/source/incoming/routing/004.php index a071f75c..d7b211c8 100644 --- a/source/incoming/routing/004.php +++ b/source/incoming/routing/004.php @@ -1,3 +1,3 @@ match(['get', 'put'], 'products', 'Product::feature'); +$routes->match(['GET', 'PUT'], 'products', 'Product::feature'); diff --git a/source/incoming/routing/026.php b/source/incoming/routing/026.php index 1ed2a8a9..b83e33fc 100644 --- a/source/incoming/routing/026.php +++ b/source/incoming/routing/026.php @@ -1,7 +1,9 @@ group('admin', static function ($routes) { - $routes->group('users', static function ($routes) { +$routes->group('admin', ['filter' => 'myfilter:config'], static function ($routes) { + $routes->get('/', 'Admin\Admin::index'); + + $routes->group('users', ['filter' => 'myfilter:region'], static function ($routes) { $routes->get('list', 'Admin\Users::list'); }); }); diff --git a/source/incoming/routing/033.php b/source/incoming/routing/033.php index ff7e995a..1f2892fc 100644 --- a/source/incoming/routing/033.php +++ b/source/incoming/routing/033.php @@ -8,7 +8,7 @@ $routes->options('from', 'to', $options); $routes->delete('from', 'to', $options); $routes->patch('from', 'to', $options); -$routes->match(['get', 'put'], 'from', 'to', $options); +$routes->match(['GET', 'PUT'], 'from', 'to', $options); $routes->resource('photos', $options); $routes->map($array, $options); $routes->group('name', $options, static function () {}); diff --git a/source/incoming/routing/047.php b/source/incoming/routing/047.php deleted file mode 100644 index d8ff31b8..00000000 --- a/source/incoming/routing/047.php +++ /dev/null @@ -1,4 +0,0 @@ -setDefaultController('Home'); diff --git a/source/incoming/routing/048.php b/source/incoming/routing/048.php deleted file mode 100644 index e926e651..00000000 --- a/source/incoming/routing/048.php +++ /dev/null @@ -1,3 +0,0 @@ -setDefaultMethod('listAll'); diff --git a/source/incoming/routing/051.php b/source/incoming/routing/051.php index 8c598c40..33852c87 100644 --- a/source/incoming/routing/051.php +++ b/source/incoming/routing/051.php @@ -10,12 +10,3 @@ class Routing extends BaseRouting public ?string $override404 = 'App\Errors::show404'; // ... } - -// In app/Config/Routes.php -// Would execute the show404 method of the App\Errors class -$routes->set404Override('App\Errors::show404'); - -// Will display a custom view -$routes->set404Override(static function () { - echo view('my_errors/not_found.html'); -}); diff --git a/source/incoming/routing/069.php b/source/incoming/routing/069.php new file mode 100644 index 00000000..24d63668 --- /dev/null +++ b/source/incoming/routing/069.php @@ -0,0 +1,13 @@ +set404Override('App\Errors::show404'); + +// Will display a custom view. +$routes->set404Override(static function () { + // If you want to get the URI segments. + $segments = request()->getUri()->getSegments(); + + return view('my_errors/not_found.html'); +}); diff --git a/source/incoming/routing/070.php b/source/incoming/routing/070.php new file mode 100644 index 00000000..f62961f1 --- /dev/null +++ b/source/incoming/routing/070.php @@ -0,0 +1,11 @@ +`_, + 我们提供了 `预加载脚本 `_。 + +需求 +----------- + +使用预加载需要一个专门的 PHP 处理器。通常情况下,Web 服务器配置为使用一个 PHP 处理器,所以一个应用程序需要一个专用的 Web 服务器。如果你想在一个 Web 服务器上为多个应用使用预加载,请配置你的服务器以使用带有多个 PHP 处理器的虚拟主机,例如多个 PHP-FPM,每个虚拟主机使用一个 PHP 处理器。预加载通过读取在 ``opcache.preload`` 中指定的文件将相关定义保留在内存中。 + +.. note:: 参见 :ref:`running-multiple-app` 以使用一个核心的 CodeIgniter4 处理多个应用。 + +配置 +------------- + +打开 ``php.ini`` 或 ``xx-opcache.ini``(如果你将 INI 配置分离开来),建议设置 ``opcache.preload=/path/to/preload.php`` 和 ``opcache.preload_user=myuser``。 + +.. note:: ``myuser`` 是在你的 Web 服务器中运行的用户。如果你想找到分离的 INI 配置的位置,只需运行 ``php --ini`` 或打开 ``phpinfo()`` 文件并搜索 *Additional .ini files parsed*。 + +确保你使用的是 appstarter 安装。如果使用手动安装,你必须更改 ``include`` 路径中的目录。 + +.. literalinclude:: preloading/001.php + +.. _deployment-to-shared-hosting-services: + +************************************* +部署到共享主机服务 +************************************* + +.. important:: + **index.php** 不再位于项目的根目录中!它已移动到 **public** 目录中,这样更安全,并能更好地分离组件。 + + 这意味着你应该配置你的 Web 服务器指向项目的 **public** 目录,而不是项目根目录。 + +指定文档根目录 +============================ + +最好的方式是在服务器配置中将文档根目录设置为 **public** 目录: + + └── example.com/ (项目目录) + └── public/ (文档根目录) + +请与你的主机服务提供商确认是否可以更改文档根目录。如果不能更改文档根目录,请参考下一种方法。 + +使用两个目录 +===================== + +第二种方式是使用两个目录,并调整路径。 +一个用于应用程序,另一个是默认的文档根目录。 + +将 **public** 目录的内容上传到 **public_html** (默认的文档根目录),其他文件上传到用于应用程序的目录:: + + ├── example.com/ (用于应用程序) + │ ├── app/ + │ ├── vendor/ (或 system/) + │ └── writable/ + └── public_html/ (默认的文档根目录) + ├── .htaccess + ├── favicon.ico + ├── index.php + └── robots.txt + +参见 +`Install CodeIgniter 4 on Shared Hosting (cPanel) `_ +获取详细信息。 + +添加 .htaccess +================ + +最后一招是在项目根目录中添加 **.htaccess** 文件。 + +不建议将项目文件夹放在文档根目录中。然而,如果没有其他选择,你可以使用这种方法。 + +按照以下方式放置你的项目文件夹,其中 **public_html** 是文档根目录,并创建 **.htaccess** 文件: + + └── public_html/ (默认的文档根目录) + └── example.com/ (项目文件夹) + ├── .htaccess + └── public/ + +并按如下内容编辑 **.htaccess**: + +.. code-block:: apache + + + RewriteEngine On + RewriteRule ^(.*)$ public/$1 [L] + + + + Require all denied + Satisfy All + + +然后,删除 **public/.htaccess** 中的重定向设置: + +.. code-block:: diff + + --- a/public/.htaccess + +++ b/public/.htaccess + @@ -16,16 +16,6 @@ Options -Indexes + # http://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritebase + # RewriteBase / + + - # Redirect Trailing Slashes... + - RewriteCond %{REQUEST_FILENAME} !-d + - RewriteCond %{REQUEST_URI} (.+)/$ + - RewriteRule ^ %1 [L,R=301] + - + - # Rewrite "www.example.com -> example.com" + - RewriteCond %{HTTPS} !=on + - RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + - RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] + - + # 检查用户是否试图访问有效文件,如图片或 css 文档,如果不是真的,则将 + # 请求发送到前端控制器 index.php diff --git a/source/installation/index.rst b/source/installation/index.rst index 39f955be..a80d2ef4 100755 --- a/source/installation/index.rst +++ b/source/installation/index.rst @@ -6,7 +6,7 @@ CodeIgniter 支持两种安装方法:手动下载和使用 `Composer `_ 都可以在线访问。 @@ -27,6 +27,7 @@ CodeIgniter 支持两种安装方法:手动下载和使用 `Composer `_。 -第一种技术描述了使用 CodeIgniter4 创建骨架项目的方法,然后你可以将其用作新 Web 应用程序的基础。 -下面描述的第二种技术允许你将 CodeIgniter4 添加到现有的 Web 应用程序中。 +第一个方法描述了如何使用 CodeIgniter4 创建一个项目骨架(App Starter),你可以将其作为新 Web 应用的基础。下面描述的第二个方法允许你将 CodeIgniter4 添加到已有的 Web 应用中。 -.. note:: 如果你使用 Git 仓库存储代码或与他人协作,那么 **vendor** 文件夹通常会被“git 忽略”。在这种情况下,当你将仓库克隆到新系统时,需要运行 ``composer update``。 +.. note:: 如果你使用 Git 仓库来存储代码或与他人协作,那么 **vendor** 文件夹通常会被 “git 忽略”。在这种情况下,当你将仓库克隆到一个新系统时,你需要执行 ``composer install``(如果你想更新所有 Composer 依赖项,则执行 ``composer update``)。 App Starter =========== @@ -48,6 +47,32 @@ App Starter 上述命令将只移除开发环境下的 Composer 软件包,这些软件包在生产环境中不需要。这将大大减少 vendor 文件夹的大小。 +安装先前版本 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +例如,你可能希望在 v4.5.0 发布后安装 v4.4.8。 + +在这种情况下,在命令中指定版本: + +.. code-block:: console + + composer create-project codeigniter4/appstarter:4.4.8 project-root + +然后,在项目根文件夹中打开 **composer.json**,并指定框架版本: + +.. code-block:: text + + "require": { + ... + "codeigniter4/framework": "4.4.8" + }, + +然后,运行 ``composer update`` 命令。 + +.. note:: 当你在 **composer.json** 中使用固定版本号如 ``"codeigniter4/framework": "4.4.8"`` 时,``composer update`` 命令将不会更新框架到最新版本。请参见 `Writing Version Constraints`_ 了解如何指定版本。 + +.. _Writing Version Constraints: https://getcomposer.org/doc/articles/versions.md#writing-version-constraints + 初始配置 --------------------- @@ -64,7 +89,25 @@ App Starter composer update -阅读 :doc:`升级说明 `,并查看已破坏的更改和增强功能。 +阅读 :doc:`升级说明 ` 和 :doc:`变更日志 <../changelogs/index>`,并检查重大变更和增强功能。 + +升级到指定版本 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +例如,你可能希望在 v4.5.0 发布后从 v4.4.7 升级到 v4.4.8。 + +在这种情况下,在项目根文件夹中打开 **composer.json**,并指定框架版本: + +.. code-block:: text + + "require": { + ... + "codeigniter4/framework": "4.4.8" + }, + +然后,运行 ``composer update`` 命令。 + +.. note:: 当你在 **composer.json** 中使用固定版本号如 ``"codeigniter4/framework": "4.4.8"`` 时,``composer update`` 命令将不会更新框架到最新版本。请参见 `Writing Version Constraints`_ 了解如何指定版本。 优点 ---- @@ -113,11 +156,11 @@ App Starter 仓库带有 ``builds`` 脚本,可在当前稳定版本和框架的 如果你想使用下一个次要版本的分支,在使用 ``builds`` 命令后手动编辑 **composer.json**。 -如果你尝试使用 ``4.4`` 分支,请将版本更改为 ``4.4.x-dev``:: +如果你尝试使用 ``4.6`` 分支,请将版本更改为 ``4.6.x-dev``:: "require": { - "php": "^7.4 || ^8.0", - "codeigniter4/codeigniter4": "4.4.x-dev" + "php": "^8.1", + "codeigniter4/codeigniter4": "4.6.x-dev" }, 然后运行 ``composer update``,以使你的 vendor 文件夹与最新的目标构建同步。然后,根据需要检查升级指南(**user_guide_src/source/installation/upgrade_{version}.rst**)并更新项目文件。 @@ -150,9 +193,9 @@ App Starter 仓库带有 ``builds`` 脚本,可在当前稳定版本和框架的 .. important:: 将应用程序部署到生产服务器时,不要忘记运行以下命令: -.. code-block:: console + .. code-block:: console - composer install --no-dev + composer install --no-dev 上述命令将只移除开发环境下的 Composer 软件包,这些软件包在生产环境中不需要。这将大大减少 vendor 文件夹的大小。 @@ -179,7 +222,23 @@ App Starter 仓库带有 ``builds`` 脚本,可在当前稳定版本和框架的 composer update -阅读 :doc:`升级说明 `,并查看已破坏的更改和增强功能。 +阅读 :doc:`升级说明 ` 和 :doc:`变更日志 <../changelogs/index>`,并检查重大变更和增强功能。 + +升级到指定版本 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +例如,你可能希望在 v4.5.0 发布后从 v4.4.7 升级到 v4.4.8。 + +在这种情况下,在项目根文件夹中打开 **composer.json**,并指定框架版本: + +.. code-block:: text + + "require": { + ... + "codeigniter4/framework": "4.4.8" + }, + +然后,运行 ``composer update`` 命令。 优点 ---- diff --git a/source/installation/installing_manual.rst b/source/installation/installing_manual.rst index 3792c3d9..420993be 100644 --- a/source/installation/installing_manual.rst +++ b/source/installation/installing_manual.rst @@ -36,7 +36,7 @@ 下载框架的新副本,然后替换 **system** 文件夹。 -阅读 :doc:`升级说明 `,并查看已破坏的更改和增强功能。 +阅读 :doc:`升级说明 ` 和 :doc:`变更日志 <../changelogs/index>`,并检查重大变更和增强功能。 优点 ==== diff --git a/source/installation/preloading/001.php b/source/installation/preloading/001.php new file mode 100644 index 00000000..059c6d84 --- /dev/null +++ b/source/installation/preloading/001.php @@ -0,0 +1,20 @@ + __DIR__ . '/system', // <== change this line to where CI is installed + // ... + ], + ]; + + // ... +} + +// ... diff --git a/source/installation/running.rst b/source/installation/running.rst index eddaf578..57addaff 100644 --- a/source/installation/running.rst +++ b/source/installation/running.rst @@ -36,7 +36,7 @@ CodeIgniter 4 应用程序可以以多种不同的方式运行:托管在 Web 配置数据库连接设置 ====================================== -如果你打算使用数据库,请使用文本编辑器打开 **app/Config/Database.php** 文件,并设置你的数据库设置。或者,你可以在 **.env** 文件中设置这些设置。 +如果你打算使用数据库,使用文本编辑器打开 **app/Config/Database.php** 文件并设置数据库配置。或者,你也可以在 **.env** 文件中设置这些配置。详细信息请参阅 :ref:`数据库配置 `。 设置为开发模式 ======================= @@ -50,6 +50,44 @@ CodeIgniter 4 应用程序可以以多种不同的方式运行:托管在 Web 如果你将使用 Web 服务器(例如 Apache 或 nginx)运行你的站点,你需要修改项目中的 **writable** 文件夹的权限,以便它可以被你的 Web 服务器使用的用户或帐户写入。 +.. _spark-phpini-check: + +检查 PHP ini 设置 +========================= + +.. versionadded:: 4.5.0 + +`PHP ini 设置`_ 更改 PHP 的行为。CodeIgniter 提供了一个命令来检查重要的 PHP 设置。 + +.. _PHP ini 设置: https://www.php.net/manual/en/ini.list.php + +.. code-block:: console + + php spark phpini:check + +*推荐* 列显示了生产环境的推荐值。它们在开发环境中可能会有所不同。 + +.. note:: + 如果你不能使用 spark 命令,可以在你的控制器中使用 ``CheckPhpIni::run(false)``。 + + 例如, + + .. code-block:: php + + +并且移除 **public/.htaccess** 中的重定向设置: + +.. code-block:: diff + + --- a/public/.htaccess + +++ b/public/.htaccess + @@ -16,16 +16,6 @@ Options -Indexes + # http://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritebase + # RewriteBase / + + - # Redirect Trailing Slashes... + - RewriteCond %{REQUEST_FILENAME} !-d + - RewriteCond %{REQUEST_URI} (.+)/$ + - RewriteRule ^ %1 [L,R=301] + - + - # Rewrite "www.example.com -> example.com" + - RewriteCond %{HTTPS} !=on + - RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + - RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] + - + # 检查用户是否尝试访问有效文件, + # 例如图像或 css 文档,如果不是真的则将请求发送到前端控制器 index.php + 使用 mod_userdir 进行托管(共享主机) ======================================= @@ -381,6 +442,12 @@ default.conf 请参阅 :ref:`处理多个环境 `。 +************************************* +部署到共享主机服务 +************************************* + +参见 :ref:`Deployment `。 + ********************* 引导应用程序 ********************* diff --git a/source/installation/troubleshooting.rst b/source/installation/troubleshooting.rst index d5bdfe65..3238f417 100755 --- a/source/installation/troubleshooting.rst +++ b/source/installation/troubleshooting.rst @@ -85,9 +85,4 @@ CodeIgniter 错误日志 ---------------------- -CodeIgniter 根据 **app/Config/Logger.php** 中的设置记录错误消息。 - -你可以调整错误阈值以查看更多或更少的消息。有关详细信息,请参阅 :ref:`日志记录 `。 - -默认配置将每日日志文件存储在 **writable/logs** 中。 -如果事情没有按你期望的那样工作,最好检查一下它们! +参阅 :ref:`codeigniter-error-logs`。 diff --git a/source/installation/upgrade_415/001.php b/source/installation/upgrade_415/001.php index 9d51911e..272f9e2f 100644 --- a/source/installation/upgrade_415/001.php +++ b/source/installation/upgrade_415/001.php @@ -9,8 +9,8 @@ class Filters extends BaseConfig // ... public $methods = [ - 'get' => ['csrf'], - 'post' => ['csrf'], + 'GET' => ['csrf'], + 'POST' => ['csrf'], ]; // ... } diff --git a/source/installation/upgrade_444.rst b/source/installation/upgrade_444.rst index c0e94707..d576671d 100644 --- a/source/installation/upgrade_444.rst +++ b/source/installation/upgrade_444.rst @@ -63,7 +63,7 @@ CURLRequest 选项 `verify` 也可以像往常一样接受 *布尔值*。 项目文件 ************* -**项目空间**(root,app,public,writable)中的一些文件已经更新。由于这些文件在 **system** 范围之外,没有你的干预不会发生改变。 +**项目空间** (root,app,public,writable)中的一些文件已经更新。由于这些文件在 **system** 范围之外,没有你的干预不会发生改变。 有一些第三方 CodeIgniter 模块可用于帮助合并对项目空间的更改:`在 Packagist 上探索 `_。 diff --git a/source/installation/upgrade_445.rst b/source/installation/upgrade_445.rst index 28a3d646..e375c897 100644 --- a/source/installation/upgrade_445.rst +++ b/source/installation/upgrade_445.rst @@ -16,7 +16,7 @@ 项目文件 ************* -**项目空间**(根目录,app,public,writable)中的一些文件接收到更新。由于这些文件在 **system** 范围之外,它们不会在没有你的干预的情况下被更改。 +**项目空间** (根目录,app,public,writable)中的一些文件接收到更新。由于这些文件在 **system** 范围之外,它们不会在没有你的干预的情况下被更改。 有一些第三方 CodeIgniter 模块可用于帮助合并对项目空间的更改:`在 Packagist 上探索 `_。 diff --git a/source/installation/upgrade_446.rst b/source/installation/upgrade_446.rst new file mode 100644 index 00000000..4faaa6b0 --- /dev/null +++ b/source/installation/upgrade_446.rst @@ -0,0 +1,50 @@ +############################# +从 4.4.5 升级到 4.4.6 +############################# + +请参阅与你的安装方法对应的升级说明。 + +- :ref:`Composer 安装 App Starter 升级 ` +- :ref:`Composer 安装 将 CodeIgniter4 添加到一个现有项目升级 ` +- :ref:`手动安装升级 ` + +.. contents:: + :local: + :depth: 2 + +**************** +重大变更 +**************** + +Time::createFromTimestamp() 时区变更 +=========================================== + +当你没有指定时区时,现在 +:ref:`Time::createFromTimestamp() ` 返回一个具有应用程序时区的 Time +实例。 + +如果你想保持时区为 UTC,你需要调用 ``setTimezone('UTC')``:: + + use CodeIgniter\I18n\Time; + + $time = Time::createFromTimestamp(1501821586)->setTimezone('UTC'); + +************* +项目文件 +************* + +**项目空间** (root, app, public, writable)中的一些文件收到了更新。由于 +这些文件位于 **system** 范围之外,没有你的干预它们不会被更改。 + +有一些第三方的 CodeIgniter 模块可以帮助合并对项目空间的更改:`在 Packagist 上探索 `_。 + +所有更改 +=========== + +这是一个 **项目空间** 内所有收到更改的文件列表; +许多将只是简单的注释或格式更改,对运行时没有影响: + +- app/Config/App.php +- app/Config/Routing.php +- app/Views/welcome_message.php +- composer.json diff --git a/source/installation/upgrade_447.rst b/source/installation/upgrade_447.rst new file mode 100644 index 00000000..47053a71 --- /dev/null +++ b/source/installation/upgrade_447.rst @@ -0,0 +1,126 @@ +############################# +从 4.4.6 升级到 4.4.7 +############################# + +请参阅与你的安装方法对应的升级说明。 + +- :ref:`Composer 安装 App Starter 升级 ` +- :ref:`Composer 安装 将 CodeIgniter4 添加到一个现有项目升级 ` +- :ref:`手动安装升级 ` + +.. contents:: + :local: + :depth: 2 + +********************** +强制文件更改 +********************** + +URI 安全性 +============ + +添加了检查 URI 中不包含不允许的字符串的功能。 +此检查等同于 CodeIgniter 3 中的 URI 安全性。 + +我们建议你启用此功能。在 **app/Config/App.php** 文件中添加以下内容:: + + public string $permittedURIChars = 'a-z 0-9~%.:_\-';. + +详情请参阅 :ref:`urls-uri-security`。 + +错误文件 +=========== + +错误页面已更新。请更新以下文件: + +- app/Views/errors/html/debug.css +- app/Views/errors/html/error_exception.php + +**************** +重大变更 +**************** + +.. _upgrade-447-filter-paths: + +控制器过滤器中的路径 +=========================== + +已修复 :doc:`../incoming/filters` 处理的 URI 路径未进行 URL 解码的错误。 + +.. note:: 请注意 :doc:`Router <../incoming/routing>` 处理 URL 解码后的 URI 路径。 + +``Config\Filters`` 中有一些地方可以指定 URI 路径。如果路径在 URL 解码后有不同的值,请将它们更改为 URL 解码后的值。 + +例如: + +.. code-block:: php + + public array $globals = [ + 'before' => [ + 'csrf' => ['except' => '%E6%97%A5%E6%9C%AC%E8%AA%9E/*'], + ], + // ... + ]; + +↓ + +.. code-block:: php + + public array $globals = [ + 'before' => [ + 'csrf' => ['except' => '日本語/*'], + ], + // ... + ]; + +Time::difference() 和夏令时 +=========================== + +在以前的版本中,当使用 ``Time::difference()`` 比较日期时,如果由于夏令时 (DST) 导致日期包含不同于 24 小时的一天,则会返回意外结果。详情请参阅 :ref:`Times and Dates 中的备注 `。 + +此错误已修复,因此在这种情况下,日期比较将被移后一日。 + +在某些不太可能的情况下,如果你希望保持以前版本的行为,请在将要比较的两个日期传递给 ``Time::difference()`` 之前,将它们的时区更改为 UTC。 + +************* +项目文件 +************* + +**项目空间** (root, app, public, writable)中的一些文件收到了更新。由于这些文件位于 **system** 范围之外,没有你的干预它们不会被更改。 + +有一些第三方的 CodeIgniter 模块可以帮助合并对项目空间的更改:`在 Packagist 上探索 `_。 + +内容变更 +=============== + +以下文件进行了重要更改(包括弃用或视觉调整),建议你将更新的版本与应用程序合并: + +配置 +------ + +- app/Config/App.php + - 添加了属性 ``$permittedURIChars``。详情请参阅 :ref:`urls-uri-security`。 + +所有更改 +=========== + +这是一个 **项目空间** 内所有收到更改的文件列表; +许多将只是简单的注释或格式更改,对运行时没有影响: + +- app/Config/App.php +- app/Config/Cache.php +- app/Config/ContentSecurityPolicy.php +- app/Config/Database.php +- app/Config/Exceptions.php +- app/Config/Filters.php +- app/Config/Format.php +- app/Config/Logger.php +- app/Config/Mimes.php +- app/Config/Routing.php +- app/Config/Toolbar.php +- app/Config/Validation.php +- app/Config/View.php +- app/Controllers/BaseController.php +- app/Views/errors/html/debug.css +- app/Views/errors/html/error_exception.php +- composer.json diff --git a/source/installation/upgrade_448.rst b/source/installation/upgrade_448.rst new file mode 100644 index 00000000..276d0da1 --- /dev/null +++ b/source/installation/upgrade_448.rst @@ -0,0 +1,31 @@ +############################# +从 4.4.7 升级到 4.4.8 +############################# + +请参阅与你的安装方法对应的升级说明。 + +- :ref:`Composer 安装 App Starter 升级 ` +- :ref:`Composer 安装 将 CodeIgniter4 添加到一个现有项目升级 ` +- :ref:`手动安装升级 ` + +.. contents:: + :local: + :depth: 2 + +************* +项目文件 +************* + +**项目空间** (root, app, public, writable)中的一些文件收到了更新。由于这些文件位于 **system** 范围之外,没有你的干预它们不会被更改。 + +有一些第三方的 CodeIgniter 模块可以帮助合并对项目空间的更改:`在 Packagist 上探索 `_。 + +所有更改 +=========== + +这是一个 **项目空间** 内所有收到更改的文件列表; +许多将只是简单的注释或格式更改,对运行时没有影响: + +- app/.htaccess +- public/.htaccess +- writable/.htaccess diff --git a/source/installation/upgrade_450.rst b/source/installation/upgrade_450.rst new file mode 100644 index 00000000..b1289120 --- /dev/null +++ b/source/installation/upgrade_450.rst @@ -0,0 +1,329 @@ +############################# +从 4.4.8 升级到 4.5.0 +############################# + +请参阅与你的安装方法对应的升级说明。 + +- :ref:`Composer 安装 App Starter 升级 ` +- :ref:`Composer 安装 将 CodeIgniter4 添加到一个现有项目升级 ` +- :ref:`手动安装升级 ` + +.. contents:: + :local: + :depth: 2 + +强制文件更改 +********************** + +index.php 和 spark +=================== + +以下文件进行了破坏性变更, +**你必须将更新的版本** 合并到你的应用程序中: + +- ``public/index.php`` +- ``spark`` + +.. important:: 如果不更新上述文件,在运行 ``composer update`` 之后 CodeIgniter 将无法正常工作。 + + 升级步骤例如如下: + + .. code-block:: console + + composer update + cp vendor/codeigniter4/framework/public/index.php public/index.php + cp vendor/codeigniter4/framework/spark spark + +破坏性变更 +**************** + +.. _upgrade-450-lowercase-http-method-name: + +小写 HTTP 方法名 +========================== + +Request::getMethod() +-------------------- + +由于历史原因,``Request::getMethod()`` 默认返回小写的 HTTP 方法名。 + +但是方法标记是区分大小写的,因为它可能用作区分大小写的方法名的对象系统的网关。按照惯例,标准化的方法都定义为全大写的 US-ASCII 字母。详情见 https://www.rfc-editor.org/rfc/rfc9110#name-overview。 + +现在,``Request::getMethod()`` 中已弃用的 ``$upper`` 参数已被移除,而 ``getMethod()`` 返回的是原样的 HTTP 方法名。即,大写形式如 "GET", "POST" 等等。 + +如果你需要小写的 HTTP 方法名,请使用 PHP 的 ``strtolower()`` 函数:: + + strtolower($request->getMethod()) + +并且在你的应用代码中应使用大写的 HTTP 方法名。 + +app/Config/Filters.php +---------------------- + +你应将 **app/Config/Filters.php** 中 ``$methods`` 的键更新为大写:: + + public array $methods = [ + 'POST' => ['invalidchars', 'csrf'], + 'GET' => ['csrf'], + ]; + +CURLRequest::request() +---------------------- + +在之前的版本中,你可以将小写的 HTTP 方法传递给 ``request()`` 方法。这个错误已被修复。 + +现在,你必须传递正确的 HTTP 方法名,如 ``GET``, ``POST``。否则你会得到错误响应:: + + $client = \Config\Services::curlrequest(); + $response = $client->request('get', 'https://www.google.com/', [ + 'http_errors' => false, + ]); + $response->getStatusCode(); // 之前版本:200 + // 现在版本:405 + +.. _upgrade-450-nested-route-groups-and-options: + +嵌套路由组和选项 +=============================== + +阻止传递给外部 ``group()`` 的选项与内部 ``group()`` 的选项合并的错误已被修复。 + +请检查并更正你的路由配置,因为这可能会更改应用的选项值。 + +例如, + +.. code-block:: php + + $routes->group('admin', ['filter' => 'csrf'], static function ($routes) { + $routes->get('/', static function () { + // ... + }); + + $routes->group('users', ['namespace' => 'Users'], static function ($routes) { + $routes->get('/', static function () { + // ... + }); + }); + }); + +现在,``csrf`` 过滤器将为 ``admin`` 和 ``admin/users`` 路由执行。 +在之前的版本中,它仅为 ``admin`` 路由执行。 +另请参阅 :ref:`routing-nesting-groups`。 + +.. _upgrade-450-filter-execution-order: + +过滤器执行顺序 +====================== + +控制器过滤器执行顺序已更改。 +如果你希望维持之前版本的执行顺序,请在 ``Config\Feature::$oldFilterOrder`` 中设置 ``true``。另请参阅 :ref:`filter-execution-order`。 + +1. 过滤器组的执行顺序已更改。 + + 前置过滤器:: + + 之前: route → globals → methods → filters + 现在: globals → methods → filters → route + + 后置过滤器:: + + 之前: route → globals → filters + 现在: route → filters → globals + +2. 在 *Route* 过滤器和 *Filters* 过滤器中的后置过滤器执行顺序现在是反向的。 + + 与以下配置有关: + + .. code-block:: php + + // 在 app/Config/Routes.php 中 + $routes->get('/', 'Home::index', ['filter' => ['route1', 'route2']]); + + // 在 app/Config/Filters.php 中 + public array $filters = [ + 'filter1' => ['before' => '*', 'after' => '*'], + 'filter2' => ['before' => '*', 'after' => '*'], + ]; + + 前置过滤器:: + + 之前: route1 → route2 → filter1 → filter2 + 现在: filter1 → filter2 → route1 → route2 + + 后置过滤器:: + + 之前: route1 → route2 → filter1 → filter2 + 现在: route2 → route1 → filter2 → filter1 + +.. _upgrade-450-api-response-trait: + +API\\ResponseTrait 和字符串数据 +================================== + +在以前的版本中,如果你将字符串数据传递给 trait 方法,即使响应格式被确定为 JSON,框架还是会返回 HTML 响应。 + +现在,如果你传递字符串数据,它将正确返回 JSON 响应。另请参阅 :ref:`api-response-trait-handling-response-types`。 + +如果你希望保持之前版本的行为,请在控制器中将 ``$stringAsHtml`` 属性设置为 ``true``。 + +FileLocator::findQualifiedNameFromPath() +======================================== + +在以前的版本中,``FileLocator::findQualifiedNameFromPath()`` 返回带有前导 ``\`` 的完全限定类名。现在,前导 ``\`` 已被移除。 + +如果你的代码依赖于带前导 ``\`` 的结果,请修正。 + +BaseModel::getIdValue() +======================= + +``BaseModel::getIdValue()`` 已更改为 ``abstract``,实现已被移除。 + +如果你扩展了 ``BaseModel``,请在子类中实现 ``getIdValue()`` 方法。 + +Factories +========= + +:doc:`../concepts/factories` 已更改为最终类。 +在极不可能的情况下,如果你继承了 Factories,请停止继承并将代码复制到你的 Factories 类中。 + +自动路由(传统) +===================== + +在以前的版本中,即使未找到相应的控制器,也可能会执行控制器过滤器。 + +此错误已被修复,现在如果未找到控制器,将抛出 ``PageNotFoundException`` 且不会执行过滤器。 + +如果你的代码依赖于此错误,例如你期望即使在不存在的页面上也会执行全局过滤器,请使用新的 :ref:`v450-required-filters`。 + +方法签名更改 +======================== + +一些方法签名已更改。扩展它们的类应更新其 API 以反映这些更改。详情请参阅 :ref:`ChangeLog `。 + +移除的弃用项 +======================== + +一些弃用项已被移除。如果你仍在使用这些项或扩展这些类,请升级你的代码。详情请参阅 :ref:`ChangeLog `。 + +破坏性改进 +********************* + +.. _upgrade-450-404-override: + +404 覆盖状态码 +======================== + +在以前的版本中,:ref:`404-override` 默认返回状态码为 ``200`` 的响应。现在它默认返回 ``404``。 + +如果你需要 ``200``,请在控制器中设置:: + + $routes->set404Override(static function () { + response()->setStatusCode(200); + + echo view('my_errors/not_found.html'); + }); + +Validation::run() 签名 +=========================== + +``Validation::run()`` 和 ``ValidationInterface::run()`` 的方法签名已更改。``$dbGroup`` 参数的 ``?string`` 类型提示已被移除。扩展的类也应移除该参数以不破坏 LSP。 + +项目文件 +************* + +**项目空间** (root, app, public, writable)中的一些文件收到了更新。由于这些文件位于 **system** 范围之外,没有你的干预它们不会被更改。 + +有一些第三方的 CodeIgniter 模块可以帮助合并对项目空间的更改:`在 Packagist 上探索 `_。 + +内容变更 +=============== + +以下文件进行了重要更改(包括弃用或视觉调整),建议将更新的版本与应用程序合并: + +配置 +------ + +app/Config/Filters.php +^^^^^^^^^^^^^^^^^^^^^^ + +已添加必需过滤器,因此做了以下更改。另请参阅 :ref:`ChangeLog `。 + +基类已更改:: + + class Filters extends \CodeIgniter\Config\Filters + +在 ``$aliases`` 属性中添加了以下项目:: + + public array $aliases = [ + // ... + 'forcehttps' => \CodeIgniter\Filters\ForceHTTPS::class, + 'pagecache' => \CodeIgniter\Filters\PageCache::class, + 'performance' => \CodeIgniter\Filters\PerformanceMetrics::class, + ]; + +添加了一个新属性 ``$required``,并设置如下:: + + public array $required = [ + 'before' => [ + 'forcehttps', // 强制全局安全请求 + 'pagecache', // 网页缓存 + ], + 'after' => [ + 'pagecache', // 网页缓存 + 'performance', // 性能指标 + 'toolbar', // 调试工具栏 + ], + ]; + +``$global['after']`` 中的 ``'toolbar'`` 被移除。 + +其他 +^^^^^^ + +- app/Config/Boot/production.php + - ``error_reporting()`` 的默认错误级别已更改为 ``E_ALL & ~E_DEPRECATED``。 +- app/Config/Cors.php + - 添加了处理 CORS 配置。 +- app/Config/Database.php + - ``$default`` 中 ``charset`` 的默认值已更改为 ``utf8mb4``。 + - ``$default`` 中 ``DBCollat`` 的默认值已更改为 ``utf8mb4_general_ci``。 + - ``$tests`` 中 ``DBCollat`` 的默认值已更改为 ``''``。 +- app/Config/Feature.php + - 添加了 ``Config\Feature::$oldFilterOrder``。另请参阅 :ref:`filter-execution-order`。 + - 添加了 ``Config\Feature::$limitZeroAsAll``。另请参阅 :ref:`v450-query-builder-limit-0-behavior`。 + - 移除了 ``Config\Feature::$multipleFilters``,因为现在 :ref:`multiple-filters` 已经默认启用。 +- app/Config/Kint.php + - 不再继承 ``BaseConfig``,因为启用 :ref:`factories-config-caching` 可能会导致错误。 +- app/Config/Optimize.php + - 添加了处理优化配置。 +- app/Config/Security.php + - 在 ``production`` 环境中将 ``$redirect`` 属性更改为 ``true``。 + +所有更改 +=========== + +这是一个 **项目空间** 内所有收到更改的文件列表; +许多将只是简单的注释或格式更改,对运行时没有影响: + +- app/Config/Autoload.php +- app/Config/Boot/production.php +- app/Config/Cache.php +- app/Config/Cors.php +- app/Config/Database.php +- app/Config/Feature.php +- app/Config/Filters.php +- app/Config/Generators.php +- app/Config/Kint.php +- app/Config/Optimize.php +- app/Config/Routing.php +- app/Config/Security.php +- app/Config/Session.php +- app/Views/errors/cli/error_exception.php +- app/Views/errors/html/error_exception.php +- app/Views/welcome_message.php +- composer.json +- env +- phpunit.xml.dist +- preload.php +- public/index.php +- spark diff --git a/source/installation/upgrade_451.rst b/source/installation/upgrade_451.rst new file mode 100644 index 00000000..312aac6a --- /dev/null +++ b/source/installation/upgrade_451.rst @@ -0,0 +1,36 @@ +############################# +从 4.5.0 升级到 4.5.1 +############################# + +请参阅与你的安装方法对应的升级说明。 + +- :ref:`Composer 安装 App Starter 升级 ` +- :ref:`Composer 安装 将 CodeIgniter4 添加到一个现有项目升级 ` +- :ref:`手动安装升级 ` + +.. contents:: + :local: + :depth: 2 + +************* +项目文件 +************* + +**项目空间** (root, app, public, writable)中的一些文件收到了更新。由于这些文件位于 **system** 范围之外,没有你的干预它们不会被更改。 + +有一些第三方的 CodeIgniter 模块可以帮助合并对项目空间的更改:`在 Packagist 上探索 `_。 + +所有更改 +=========== + +这是一个 **项目空间** 内所有收到更改的文件列表; +许多将只是简单的注释或格式更改,对运行时没有影响: + +- .gitignore +- composer.json +- phpunit.xml.dist +- tests/.htaccess +- tests/index.html +- writable/debugbar/.gitkeep (已移除) +- writable/debugbar/index.html +- writable/index.html diff --git a/source/installation/upgrade_4xx.rst b/source/installation/upgrade_4xx.rst index 2e5718e0..033d7b91 100755 --- a/source/installation/upgrade_4xx.rst +++ b/source/installation/upgrade_4xx.rst @@ -30,11 +30,18 @@ CodeIgniter 4 是框架的重写,并且不向后兼容。将你的应用程序 命名空间 ========== -- CI4 是为 PHP 7.4+ 构建的,框架中的所有内容都使用了命名空间,除了 helper 和 lang 文件。 +- CI4 是为 PHP 8.1+ 构建的,框架中的所有内容都使用了命名空间,除了 helper 和 lang 文件。 应用程序结构 ===================== +.. important:: + **index.php** 不再位于项目的根目录!为了更好的安全性和组件分离,它已被移到 **public** 文件夹内。 + + 这意味着你需要配置你的 Web 服务器指向你项目的 **public** 文件夹,而不是项目根目录。 + + 如果你使用共享主机,参见 :ref:`deployment-to-shared-hosting-services`。 + - **application** 文件夹重命名为 **app**,框架仍然有 **system** 文件夹,与以前的解释相同。 - 框架现在提供了 **public** 文件夹,旨在作为你的应用程序的文档根目录。 - ``defined('BASEPATH') OR exit('No direct script access allowed');`` 这一行不是必需的,因为在默认配置下, **public** 文件夹之外的文件不可访问。 @@ -66,6 +73,17 @@ CodeIgniter 4 是框架的重写,并且不向后兼容。将你的应用程序 upgrade_views upgrade_controllers +核心类更改 +================== + +- Input + - CI3 的 `Input `_ + 对应于 CI4 的 :doc:`IncomingRequest `。 + - 因为历史原因,CI3 和 CI4 使用了不正确的 HTTP 方法名称,比如 "get", "post"。从 v4.5.0 开始,CI4 使用了正确的 HTTP 方法名称,比如 "GET", "POST"。 +- Output + - CI3 的 `Output `_ + 对应于 CI4 的 :doc:`Responses `。 + 类加载 ============= @@ -102,8 +120,8 @@ CodeIgniter 4 是框架的重写,并且不向后兼容。将你的应用程序 - CI3 的 `String Helper `_ 函数 在 CI4 的 :doc:`../helpers/text_helper` 中。 - 在 CI4 中, ``redirect()`` 与 CI3 中的完全不同。 - - `redirect() 文档 CodeIgniter 3.X `_ - - `redirect() 文档 CodeIgniter 4.X <../general/common_functions.html#redirect>`_ + - `redirect() 文档 CodeIgniter 3.x `_ + - `redirect() 文档 CodeIgniter 4.x <../general/common_functions.html#redirect>`_ - 在 CI4 中,:php:func:`redirect()` 返回一个 ``RedirectResponse`` 实例,而不是重定向并终止脚本执行。你必须从控制器或控制器过滤器中返回它。 - 在调用 ``redirect()`` 之前设置的 Cookie 和 Header 不会自动携带到 ``RedirectResponse``。如果你想发送它们,你需要手动调用 ``withCookies()`` 或 ``withHeaders()``。 - 你需要将 CI3 的 ``redirect('login/form')`` 改为 ``return redirect()->to('login/form')``。 @@ -153,8 +171,6 @@ CodeIgniter 4 是框架的重写,并且不向后兼容。将你的应用程序 `引用通告 `_, `XML-RPC /服务器 `_ 和 `Zip 编码 `_。 -- CI3 的 `Input `_ 对应于 CI4 的 :doc:`传入请求 `。 -- CI3 的 `Output `_ 对应于 CI4 的 :doc:`响应 `。 - 存在于两个 CodeIgniter 版本中的所有其他库都可以通过一些调整来升级。 最重要和使用最广泛的库都有一个升级指南,它将通过简单的步骤和示例帮助你调整代码。 @@ -167,6 +183,7 @@ CodeIgniter 4 是框架的重写,并且不向后兼容。将你的应用程序 upgrade_encryption upgrade_file_upload upgrade_html_tables + upgrade_images upgrade_localization upgrade_migrations upgrade_pagination diff --git a/source/installation/upgrade_configuration.rst b/source/installation/upgrade_configuration.rst index 208d38c1..482d6b59 100644 --- a/source/installation/upgrade_configuration.rst +++ b/source/installation/upgrade_configuration.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X 配置文档 `_ -- :doc:`CodeIgniter 4.X 配置文档 ` +- `CodeIgniter 3.x 配置文档 `_ +- :doc:`CodeIgniter 4.x 配置文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_controllers.rst b/source/installation/upgrade_controllers.rst index 7d9d1e81..83757f50 100644 --- a/source/installation/upgrade_controllers.rst +++ b/source/installation/upgrade_controllers.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X 控制器文档 `_ -- :doc:`CodeIgniter 4.X 控制器文档 ` +- `CodeIgniter 3.x 控制器文档 `_ +- :doc:`CodeIgniter 4.x 控制器文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_database.rst b/source/installation/upgrade_database.rst index 0fde763d..b832be5e 100644 --- a/source/installation/upgrade_database.rst +++ b/source/installation/upgrade_database.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X 数据库参考文档 `_ -- :doc:`CodeIgniter 4.X 使用数据库文档 ` +- `CodeIgniter 3.x 数据库参考文档 `_ +- :doc:`CodeIgniter 4.x 使用数据库文档 ` 变更点 ===================== @@ -42,6 +42,7 @@ - ``$this->db->having('user_id', 45);`` 改为 ``$builder->having('user_id', 45);`` 6. CI4 不提供 CI3 中已知的`数据库缓存 `_ 层,所以如果需要缓存结果,请改用 :doc:`../libraries/caching`。 +7. 如果你在 Query Builder 中使用 ``limit(0)``,由于一个 bug,CI4 会返回所有记录而不是没有记录。但从 v4.5.0 开始,你可以通过一个设置来改变这种不正确的行为。所以请更改该设置。详细信息参见 :ref:`v450-query-builder-limit-0-behavior`。 代码示例 ============ diff --git a/source/installation/upgrade_emails.rst b/source/installation/upgrade_emails.rst index ad1318af..1aa0eebc 100644 --- a/source/installation/upgrade_emails.rst +++ b/source/installation/upgrade_emails.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X 邮件文档 `_ -- :doc:`CodeIgniter 4.X 邮件文档 ` +- `CodeIgniter 3.x 邮件文档 `_ +- :doc:`CodeIgniter 4.x 邮件文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_encryption.rst b/source/installation/upgrade_encryption.rst index 9d95be32..db5af81d 100644 --- a/source/installation/upgrade_encryption.rst +++ b/source/installation/upgrade_encryption.rst @@ -8,8 +8,8 @@ 文档 ************** -- `CodeIgniter 3.X 加密库文档 `_ -- :doc:`CodeIgniter 4.X 加密服务文档 ` +- `CodeIgniter 3.x 加密库文档 `_ +- :doc:`CodeIgniter 4.x 加密服务文档 ` 变更点 ********************* diff --git a/source/installation/upgrade_file_upload.rst b/source/installation/upgrade_file_upload.rst index 41b1081b..7c0912cc 100644 --- a/source/installation/upgrade_file_upload.rst +++ b/source/installation/upgrade_file_upload.rst @@ -7,8 +7,8 @@ 文档 ============== -- `CodeIgniter 3.X 文件上传类文档 `_ -- :doc:`CodeIgniter 4.X 上传文件处理文档 ` +- `CodeIgniter 3.x 文件上传类文档 `_ +- :doc:`CodeIgniter 4.x 上传文件处理文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_file_upload/001.php b/source/installation/upgrade_file_upload/001.php index c4c6d50d..ca2c8537 100644 --- a/source/installation/upgrade_file_upload/001.php +++ b/source/installation/upgrade_file_upload/001.php @@ -11,7 +11,7 @@ public function index() public function do_upload() { - $this->validate([ + $this->validateData([], [ 'userfile' => [ 'uploaded[userfile]', 'max_size[userfile,100]', diff --git a/source/installation/upgrade_html_tables.rst b/source/installation/upgrade_html_tables.rst index d0224913..2bc8158d 100644 --- a/source/installation/upgrade_html_tables.rst +++ b/source/installation/upgrade_html_tables.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X HTML表格文档 `_ -- :doc:`CodeIgniter 4.X HTML表格文档 ` +- `CodeIgniter 3.x HTML表格文档 `_ +- :doc:`CodeIgniter 4.x HTML表格文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_images.rst b/source/installation/upgrade_images.rst new file mode 100644 index 00000000..daa89aa1 --- /dev/null +++ b/source/installation/upgrade_images.rst @@ -0,0 +1,39 @@ +升级图像处理类 +################################ + +.. contents:: + :local: + :depth: 2 + +文档 +============== + +- `CodeIgniter 3.x 图像处理类文档 `_ +- :doc:`CodeIgniter 4.x 图像处理类文档 <../libraries/images>` + +变更内容 +===================== +- 在 CI3 中传递给构造函数或 ``initialize()`` 方法的首选项已更改为在 CI4 中的新方法中指定。 +- 一些首选项如 ``create_thumb`` 被移除了。 +- 在 CI4 中,必须调用 ``save()`` 方法来保存处理后的图像。 +- ``display_errors()`` 已被移除,如果发生错误,将抛出异常。 + +升级指南 +============= +1. 在你的类中,将 ``$this->load->library('image_lib');`` 更改为 + ``$image = \Config\Services::image();``。 +2. 更改传递给构造函数或 ``initialize()`` 方法的首选项为在相应方法中指定。 +3. 调用 ``save()`` 方法保存文件。 + +代码示例 +============ + +CodeIgniter 版本 3.x +------------------------ + +.. literalinclude:: upgrade_images/ci3sample/001.php + +CodeIgniter 版本 4.x +----------------------- + +.. literalinclude:: upgrade_images/001.php diff --git a/source/installation/upgrade_images/001.php b/source/installation/upgrade_images/001.php new file mode 100644 index 00000000..c40e9b80 --- /dev/null +++ b/source/installation/upgrade_images/001.php @@ -0,0 +1,8 @@ +withFile('/path/to/image/mypic.jpg') + ->resize(75, 50, true) + ->save('/path/to/image/mypic_thumb.jpg'); diff --git a/source/installation/upgrade_images/ci3sample/001.php b/source/installation/upgrade_images/ci3sample/001.php new file mode 100644 index 00000000..94a8553a --- /dev/null +++ b/source/installation/upgrade_images/ci3sample/001.php @@ -0,0 +1,12 @@ +load->library('image_lib', $config); + +$this->image_lib->resize(); diff --git a/source/installation/upgrade_localization.rst b/source/installation/upgrade_localization.rst index 31c119ba..13ff8e67 100644 --- a/source/installation/upgrade_localization.rst +++ b/source/installation/upgrade_localization.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X 语言文档 `_ -- :doc:`CodeIgniter 4.X 本地化文档 ` +- `CodeIgniter 3.x 语言文档 `_ +- :doc:`CodeIgniter 4.x 本地化文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_migrations.rst b/source/installation/upgrade_migrations.rst index d295b906..6608b664 100644 --- a/source/installation/upgrade_migrations.rst +++ b/source/installation/upgrade_migrations.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X 数据库迁移文档 `_ -- :doc:`CodeIgniter 4.X 数据库迁移文档 ` +- `CodeIgniter 3.x 数据库迁移文档 `_ +- :doc:`CodeIgniter 4.x 数据库迁移文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_models.rst b/source/installation/upgrade_models.rst index 37f84227..4706ca5e 100644 --- a/source/installation/upgrade_models.rst +++ b/source/installation/upgrade_models.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X 模型文档 `_ -- :doc:`CodeIgniter 4.X 模型文档 ` +- `CodeIgniter 3.x 模型文档 `_ +- :doc:`CodeIgniter 4.x 模型文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_pagination.rst b/source/installation/upgrade_pagination.rst index 3b13ee88..33051dec 100644 --- a/source/installation/upgrade_pagination.rst +++ b/source/installation/upgrade_pagination.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X 分页类文档 `_ -- :doc:`CodeIgniter 4.X 分页文档 ` +- `CodeIgniter 3.x 分页类文档 `_ +- :doc:`CodeIgniter 4.x 分页文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_responses.rst b/source/installation/upgrade_responses.rst index df02ebd1..585c2ac4 100644 --- a/source/installation/upgrade_responses.rst +++ b/source/installation/upgrade_responses.rst @@ -7,8 +7,8 @@ 文档 ============== -- `CodeIgniter 3.X 输出类文档 `_ -- :doc:`CodeIgniter 4.X HTTP 响应文档 ` +- `CodeIgniter 3.x 输出类文档 `_ +- :doc:`CodeIgniter 4.x HTTP 响应文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_routing.rst b/source/installation/upgrade_routing.rst index 82229ad0..a4e5e620 100644 --- a/source/installation/upgrade_routing.rst +++ b/source/installation/upgrade_routing.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X URI 路由文档 `_ -- :doc:`CodeIgniter 4.X URI 路由文档 ` +- `CodeIgniter 3.x URI 路由文档 `_ +- :doc:`CodeIgniter 4.x URI 路由文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_security.rst b/source/installation/upgrade_security.rst index 11aaf116..f2ca7087 100644 --- a/source/installation/upgrade_security.rst +++ b/source/installation/upgrade_security.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X 安全类文档 `_ -- :doc:`CodeIgniter 4.X 安全性文档 ` +- `CodeIgniter 3.x 安全类文档 `_ +- :doc:`CodeIgniter 4.x 安全性文档 ` .. note:: 如果使用 :doc:`../helpers/form_helper` 并全局启用 CSRF 过滤器,那么 :php:func:`form_open()` 将自动在表单中插入隐藏的 CSRF 字段。所以你不需要自行升级这个。 diff --git a/source/installation/upgrade_sessions.rst b/source/installation/upgrade_sessions.rst index dbe4d307..d3a72c10 100644 --- a/source/installation/upgrade_sessions.rst +++ b/source/installation/upgrade_sessions.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X Session 库文档 `_ -- :doc:`CodeIgniter 4.X Session 库文档 ` +- `CodeIgniter 3.x Session 库文档 `_ +- :doc:`CodeIgniter 4.x Session 库文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_validations.rst b/source/installation/upgrade_validations.rst index 1641ae28..2191c778 100644 --- a/source/installation/upgrade_validations.rst +++ b/source/installation/upgrade_validations.rst @@ -8,20 +8,21 @@ 库文档 ========================= -- `CodeIgniter 3.X 表单验证文档 `_ -- :doc:`CodeIgniter 4.X 验证文档 ` +- `CodeIgniter 3.x 表单验证文档 `_ +- :doc:`CodeIgniter 4.x 验证文档 ` 变更点 ===================== -- 如果要更改验证错误显示,必须设置 CI4 :ref:`验证视图模板 `。 -- CI4 验证没有 CI3 的回调和可调用函数。 - 请使用 :ref:`规则类 ` 或 - :ref:`闭包规则 ` - 代替。 -- 在 CI3 中,回调/可调用规则具有优先级,但在 CI4 中,闭包规则没有优先级,并且按照它们在列表中的顺序进行检查。 -- CI4 验证格式规则不允许为空字符串。 -- CI4 验证永远不会改变你的数据。 -- 从 v4.3.0 开始,引入了 :php:func:`validation_errors()`,但 API 与 CI3 的不同。 +- 如果你想更改验证错误的显示方式,你需要设置 CI4 的 :ref:`验证视图模板 `。 +- CI4 验证中没有 CI3 中的 `回调 `。 + 请使用 :ref:`Callable Rules ` (从 v4.5.0 开始)或 + :ref:`Closure Rules ` (从 v4.3.0 开始)或 + :ref:`Rule Classes ` 代替。 +- 在 CI3 中,Callbacks/Callable 规则有优先级,但在 CI4 中,Closure/Callable 规则没有优先级,按列出的顺序进行检查。 +- 从 v4.5.0 开始引入了 :ref:`Callable Rules `,但它与 CI3 的 `Callable `_ 有些不同。 +- CI4 验证格式规则不允许空字符串。 +- CI4 验证永远不会更改你的数据。 +- 从 v4.3.0 开始,引入了 :php:func:`validation_errors()`,但其 API 与 CI3 的不同。 升级指南 ============= @@ -31,9 +32,10 @@ 2. 在控制器中进行更改: - - ``$this->load->helper(array('form', 'url'));`` 改为 ``helper(['form', 'url']);`` + - ``$this->load->helper(array('form', 'url'));`` 改为 ``helper('form');`` - 移除 ``$this->load->library('form_validation');`` - - ``if ($this->form_validation->run() == FALSE)`` 改为 ``if (! $this->validate([]))`` + - ``if ($this->form_validation->run() == FALSE)`` 改为 ``if (! $this->validateData($data, $rules))`` + 其中 ``$data`` 是要验证的数据,通常是 POST 数据 ``$this->request->getPost()``。 - ``$this->load->view('myform');`` 改为 ``return view('myform', ['validation' => $this->validator,]);`` 3. 必须更改验证规则。新语法是在控制器中将规则设置为数组: diff --git a/source/installation/upgrade_validations/001.php b/source/installation/upgrade_validations/001.php index 36ec0394..0ba9b034 100644 --- a/source/installation/upgrade_validations/001.php +++ b/source/installation/upgrade_validations/001.php @@ -1,6 +1,6 @@ validate([ +$isValid = $this->validateData($data, [ 'name' => 'required|min_length[3]', 'email' => 'required|valid_email', 'phone' => 'required|numeric|max_length[10]', diff --git a/source/installation/upgrade_validations/002.php b/source/installation/upgrade_validations/002.php index 38e81eb2..b3cd4f9d 100644 --- a/source/installation/upgrade_validations/002.php +++ b/source/installation/upgrade_validations/002.php @@ -8,9 +8,11 @@ class Form extends Controller { public function index() { - helper(['form', 'url']); + helper('form'); - if (! $this->validate([ + $data = $this->request->getPost(); + + if (! $this->validateData($data, [ // Validation rules ])) { return view('myform'); diff --git a/source/installation/upgrade_view_parser.rst b/source/installation/upgrade_view_parser.rst index 21dad65c..486880dd 100644 --- a/source/installation/upgrade_view_parser.rst +++ b/source/installation/upgrade_view_parser.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X 模板解析器文档 `_ -- :doc:`CodeIgniter 4.X 视图解析器文档 ` +- `CodeIgniter 3.x 模板解析器文档 `_ +- :doc:`CodeIgniter 4.x 视图解析器文档 ` 变更点 ===================== diff --git a/source/installation/upgrade_views.rst b/source/installation/upgrade_views.rst index 6cd6951b..42d36289 100644 --- a/source/installation/upgrade_views.rst +++ b/source/installation/upgrade_views.rst @@ -8,8 +8,8 @@ 文档 ============== -- `CodeIgniter 3.X 视图文档 `_ -- :doc:`CodeIgniter 4.X 视图文档 ` +- `CodeIgniter 3.x 视图文档 `_ +- :doc:`CodeIgniter 4.x 视图文档 ` 变更点 ===================== diff --git a/source/installation/upgrading.rst b/source/installation/upgrading.rst index baba066a..c8cc7828 100755 --- a/source/installation/upgrading.rst +++ b/source/installation/upgrading.rst @@ -14,6 +14,11 @@ backward_compatibility_notes + upgrade_451 + upgrade_450 + upgrade_448 + upgrade_447 + upgrade_446 upgrade_445 upgrade_444 upgrade_443 diff --git a/source/intro/index.rst b/source/intro/index.rst index a3f3e9ba..7016384f 100755 --- a/source/intro/index.rst +++ b/source/intro/index.rst @@ -2,9 +2,15 @@ 欢迎使用 CodeIgniter4 ####################### -CodeIgniter 是一套给 PHP 网站开发者使用的应用程序开发框架和工具包。它的目标是让你能够更快速的开发,它提供了日常任务中所需的大量类库,以及简单的接口和逻辑结构。通过减少代码量,CodeIgniter 让你更加专注于你的创造性工作。 +CodeIgniter 是一套轻量、快速、灵活且安全的 PHP 全栈 Web 框架。 -CodeIgniter 将尽可能的保持其灵活性,以允许你以喜欢的方式工作,而不是被迫以其它方式工作。框架可以轻松扩展或替换核心部件,使系统按你期望的方式工作。简而言之,CodeIgniter 是一个可扩展的框架,它试图提供你所需的工具,同时让你避免踩坑。 +CodeIgniter 也是一套给 PHP 网站开发者使用的应用程序开发框架和工具包。它的目标是让你能够更快速的开发,它提供了日常任务中所需的大量类库,以及简单的接口和逻辑结构。 + +通过减少代码量,CodeIgniter 让你更加专注于你的创造性工作。 + +CodeIgniter 将尽可能的保持其灵活性,以允许你以喜欢的方式工作,而不是被迫以其它方式工作。框架可以轻松扩展或替换核心部件,使系统按你期望的方式工作。 + +简而言之,CodeIgniter 是一个可扩展的框架,它试图提供你所需的工具,同时让你避免踩坑。 ************************** CodeIgniter 适合你吗? @@ -14,10 +20,11 @@ CodeIgniter 适合你吗? - 你想要一个小巧的框架。 - 你需要出色的性能。 +- 你想要一个包含多种特性和技术的框架,以便你轻松地执行良好的安全实践。 +- 你想要一个没有魔法的框架,能够轻松找到并阅读源代码。 - 你想要一个几乎 0 配置的框架。 -- 你想要一个不需使用命令行的框架。 - 你想要一个不想被编码规则的条条框框限制住的框架。 -- 你对 PEAR 这种庞然大物不感兴趣。 +- 你想要一个易于创建测试代码的框架。 - 你不想被迫学习一种新的模板语言(当然如果你喜欢,你可以选择一个模板解析器)。 - 你不喜欢复杂,追求简单。 - 你需要清晰、全面的文档。 diff --git a/source/intro/requirements.rst b/source/intro/requirements.rst index 1e226a95..0145ccaa 100755 --- a/source/intro/requirements.rst +++ b/source/intro/requirements.rst @@ -10,7 +10,7 @@ PHP 及所需扩展 *************************** -需要 `PHP `_ 7.4 或更高版本,并启用以下 PHP 扩展: +需要 `PHP `_ 8.1 或更高版本,并启用以下 PHP 扩展: - `intl `_ - `mbstring `_ @@ -18,6 +18,12 @@ PHP 及所需扩展 .. warning:: PHP 7.4 的生命周期结束日期是 2022 年 11 月 28 日。如果你仍在使用 PHP 7.4,应立即升级。PHP 8.0 的生命周期结束日期将是 2023 年 11 月 26 日。 +.. warning:: + - PHP 7.4 的生命周期结束日期是 2022 年 11 月 28 日。 + - PHP 8.0 的生命周期结束日期是 2023 年 11 月 26 日。 + - 如果你仍在使用 PHP 7.4 或 8.0,你应该立即升级。 + - PHP 8.1 的生命周期结束日期将是 2024 年 11 月 25 日。 + *********************** 可选的 PHP 扩展 *********************** @@ -54,7 +60,7 @@ PHP 及所需扩展 - MySQL,通过 ``MySQLi`` 驱动程序(仅版本 5.1 及以上) - PostgreSQL,通过 ``Postgre`` 驱动程序(仅版本 7.4 及以上) - SQLite3,通过 ``SQLite3`` 驱动程序 - - Microsoft SQL Server,通过 ``SQLSRV`` 驱动程序(仅版本 2005 及以上) + - Microsoft SQL Server,通过 ``SQLSRV`` 驱动程序(仅版本 2012 及以上) - Oracle 数据库,通过 ``OCI8`` 驱动程序(仅版本 12.1 及以上) 并非所有驱动程序都已为 CodeIgniter4 转换/重写。 diff --git a/source/libraries/cookies/004.php b/source/libraries/cookies/004.php index cce1ea2c..7804d276 100644 --- a/source/libraries/cookies/004.php +++ b/source/libraries/cookies/004.php @@ -22,7 +22,7 @@ $cookie->getName(); // 'remember_token' $cookie->getPrefix(); // '__Secure-' $cookie->getPrefixedName(); // '__Secure-remember_token' -$cookie->getExpiresTimestamp(); // Unix timestamp +$cookie->getExpiresTimestamp(); // UNIX timestamp $cookie->getExpiresString(); // 'Fri, 14-Feb-2025 00:00:00 GMT' $cookie->isExpired(); // false $cookie->getMaxAge(); // the difference from time() to expires diff --git a/source/libraries/cors.rst b/source/libraries/cors.rst new file mode 100644 index 00000000..65945936 --- /dev/null +++ b/source/libraries/cors.rst @@ -0,0 +1,132 @@ +#################################### +跨域资源共享 (CORS) +#################################### + +.. versionadded:: 4.5.0 + +跨域资源共享 (CORS) 是一种基于 HTTP 头的安全机制,允许服务器指示浏览器应允许从其自身以外的任何来源(域名、协议或端口)加载资源。 + +CORS 通过在 HTTP 请求和响应中添加头来工作,以指示请求的资源是否可以跨不同来源共享,从而帮助防止恶意攻击,如跨站请求伪造 (CSRF) 和数据盗窃。 + +如果你不熟悉 CORS 和 CORS 头,请阅读 `MDN 上的 CORS 文档`_。 + +.. _MDN 上的 CORS 文档: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS#http_%E5%93%8D%E5%BA%94%E6%A0%87%E5%A4%B4%E5%AD%97%E6%AE%B5 + +CodeIgniter 提供了 CORS 过滤器和 helper 类。 + +.. contents:: + :local: + :depth: 2 + +**************** +配置 CORS +**************** + +设置默认配置 +====================== + +可以通过 **app/Config/Cors.php** 配置 CORS。 + +至少需要设置 ``$default`` 属性中的以下项目: + +- ``allowedOrigins``: 明确列出你想要允许的来源。 +- ``allowedHeaders``: 明确列出你想要允许的 HTTP 头。 +- ``allowedMethods``: 明确列出你想要允许的 HTTP 方法。 + +.. warning:: 基于最小特权原则,只应允许必要的最小来源、方法和头。 + +如果你在跨域请求中发送凭证(例如,cookies),请将 ``supportsCredentials`` 设置为 ``true``。 + +启用 CORS +============= + +要启用 CORS,你需要做两件事: + +1. 为允许 CORS 的路由指定 ``cors`` 过滤器。 +2. 为 CORS 预检请求添加 **OPTIONS** 路由。 + +设置路由 +------------------ + +你可以在 **app/Config/Routes.php** 中为路由设置 ``cors`` 过滤器。 + +例如, + +.. literalinclude:: cors/001.php + +不要忘记为预检请求添加 OPTIONS 路由。因为如果路由不存在,控制器过滤器(必需过滤器除外)将不起作用。 + +CORS 过滤器处理所有预检请求,因此通常不会调用 OPTIONS 路由的闭包控制器。 + +在 Config\\Filters 中设置 +------------------------- + +或者,你可以在 **app/Config/Filters.php** 中为 URI 路径设置 ``cors`` 过滤器。 + +例如, + +.. literalinclude:: cors/002.php + +不要忘记为预检请求添加 OPTIONS 路由。因为如果路由不存在,控制器过滤器(必需过滤器除外)将不起作用。 + +例如, + +.. literalinclude:: cors/003.php + +CORS 过滤器处理所有预检请求,因此通常不会调用 OPTIONS 路由的闭包控制器。 + +检查路由和过滤器 +=========================== + +配置完成后,你可以使用 :ref:`routing-spark-routes` 命令检查路由和过滤器。 + +设置其他配置 +====================== + +如果你想使用不同于默认配置的配置,请在 **app/Config/Cors.php** 中添加一个属性。 + +例如,添加 ``$api`` 属性。 + +.. literalinclude:: cors/004.php + +属性名称(在上述示例中为 ``api``)将成为配置名称。 + +然后,像 ``cors:api`` 一样将属性名称指定为过滤器参数: + +.. literalinclude:: cors/005.php + +你也可以使用 :ref:`filters-filters-filter-arguments`。 + +*************** +类参考 +*************** + +.. php:namespace:: CodeIgniter\HTTP + +.. php:class:: Cors + +.. php:method:: addResponseHeaders(RequestInterface $request, ResponseInterface $response): ResponseInterface + + :param RequestInterface $request: 请求实例 + :param ResponseInterface $response: 响应实例 + :returns: 响应实例 + :rtype: ResponseInterface + + 添加 CORS 的响应头。 + +.. php:method:: handlePreflightRequest(RequestInterface $request, ResponseInterface $response): ResponseInterface + + :param RequestInterface $request: 请求实例 + :param ResponseInterface $response: 响应实例 + :returns: 响应实例 + :rtype: ResponseInterface + + 处理预检请求。 + +.. php:method:: isPreflightRequest(IncomingRequest $request): bool + + :param IncomingRequest $request: 请求实例 + :returns: 如果是预检请求则返回 True。 + :rtype: bool + + 检查请求是否为预检请求。 diff --git a/source/libraries/cors/001.php b/source/libraries/cors/001.php new file mode 100644 index 00000000..a64583a9 --- /dev/null +++ b/source/libraries/cors/001.php @@ -0,0 +1,18 @@ +group('', ['filter' => 'cors'], static function (RouteCollection $routes): void { + $routes->resource('product'); + + $routes->options('product', static function () { + // Implement processing for normal non-preflight OPTIONS requests, + // if necessary. + $response = response(); + $response->setStatusCode(204); + $response->setHeader('Allow:', 'OPTIONS, GET, POST, PUT, PATCH, DELETE'); + + return $response; + }); + $routes->options('product/(:any)', static function () {}); +}); diff --git a/source/libraries/cors/002.php b/source/libraries/cors/002.php new file mode 100644 index 00000000..1229eaac --- /dev/null +++ b/source/libraries/cors/002.php @@ -0,0 +1,19 @@ + [ + 'before' => ['api/*'], + 'after' => ['api/*'], + ], + ]; +} diff --git a/source/libraries/cors/003.php b/source/libraries/cors/003.php new file mode 100644 index 00000000..7fcefa26 --- /dev/null +++ b/source/libraries/cors/003.php @@ -0,0 +1,7 @@ +group('', ['filter' => 'cors'], static function (RouteCollection $routes): void { + $routes->options('api/(:any)', static function () {}); +}); diff --git a/source/libraries/cors/004.php b/source/libraries/cors/004.php new file mode 100644 index 00000000..c0646219 --- /dev/null +++ b/source/libraries/cors/004.php @@ -0,0 +1,25 @@ + ['https://app.example.com'], + 'allowedOriginsPatterns' => [], + 'supportsCredentials' => true, + 'allowedHeaders' => ['Authorization', 'Content-Type'], + 'exposedHeaders' => [], + 'allowedMethods' => ['GET', 'POST', 'PUT', 'DELETE'], + 'maxAge' => 7200, + ]; +} diff --git a/source/libraries/cors/005.php b/source/libraries/cors/005.php new file mode 100644 index 00000000..e9e4f867 --- /dev/null +++ b/source/libraries/cors/005.php @@ -0,0 +1,10 @@ +group('api', ['filter' => 'cors:api'], static function (RouteCollection $routes): void { + $routes->resource('user'); + + $routes->options('user', static function () {}); + $routes->options('user/(:any)', static function () {}); +}); diff --git a/source/libraries/curlrequest.rst b/source/libraries/curlrequest.rst index c1b00e16..493d323c 100755 --- a/source/libraries/curlrequest.rst +++ b/source/libraries/curlrequest.rst @@ -131,15 +131,17 @@ CURLRequest 配置 allow_redirects =============== -默认情况下,cURL 将遵循远程服务器返回的所有“Location:”标头。``allow_redirects`` 选项允许你修改此行为。 +默认情况下,cURL 不会跟随远程服务器发送回的任何 "Location:" 头。``allow_redirects`` 选项允许你修改这种行为。 -如果将值设置为 ``false``,则它将不会遵循任何重定向: +如果你将该值设置为 ``true``,那么它将会跟随重定向: -.. literalinclude:: curlrequest/013.php +.. literalinclude:: curlrequest/014.php -将其设置为 ``true`` 将对请求应用默认设置: +.. warning:: 请注意,启用重定向可能会重定向到你未预期的 URL,并可能导致 SSRF 攻击。 -.. literalinclude:: curlrequest/014.php +将其设置为 ``false`` 将对请求应用默认设置: + +.. literalinclude:: curlrequest/013.php 你可以将数组作为 ``allow_redirects`` 选项的值传递,以指定新的设置以代替默认设置: diff --git a/source/libraries/curlrequest/014.php b/source/libraries/curlrequest/014.php index 410ed998..25e3176b 100644 --- a/source/libraries/curlrequest/014.php +++ b/source/libraries/curlrequest/014.php @@ -3,7 +3,7 @@ $client->request('GET', 'http://example.com', ['allow_redirects' => true]); /* * Sets the following defaults: - * 'max' => 5, // Maximum number of redirects to follow before stopping - * 'strict' => true, // Ensure POST requests stay POST requests through redirects - * 'protocols' => ['http', 'https'] // Restrict redirects to one or more protocols + * 'max' => 5, // Maximum number of redirects to follow before stopping + * 'strict' => true, // Ensure POST requests stay POST requests through redirects + * 'protocols' => ['http', 'https'] // Restrict redirects to one or more protocols */ diff --git a/source/libraries/curlrequest/017.php b/source/libraries/curlrequest/017.php index 3f6012cf..c88ba46b 100644 --- a/source/libraries/curlrequest/017.php +++ b/source/libraries/curlrequest/017.php @@ -1,3 +1,3 @@ setBody($body)->request('put', 'http://example.com'); +$client->setBody($body)->request('PUT', 'http://example.com'); diff --git a/source/libraries/curlrequest/018.php b/source/libraries/curlrequest/018.php index 16fba53e..739784bc 100644 --- a/source/libraries/curlrequest/018.php +++ b/source/libraries/curlrequest/018.php @@ -1,3 +1,3 @@ request('put', 'http://example.com', ['body' => $body]); +$client->request('PUT', 'http://example.com', ['body' => $body]); diff --git a/source/libraries/curlrequest/019.php b/source/libraries/curlrequest/019.php index 0382d08e..9c74dd29 100644 --- a/source/libraries/curlrequest/019.php +++ b/source/libraries/curlrequest/019.php @@ -1,3 +1,3 @@ request('get', '/', ['cert' => ['/path/server.pem', 'password']]); +$client->request('GET', '/', ['cert' => ['/path/server.pem', 'password']]); diff --git a/source/libraries/curlrequest/025.php b/source/libraries/curlrequest/025.php index acc8b400..5f1818a7 100644 --- a/source/libraries/curlrequest/025.php +++ b/source/libraries/curlrequest/025.php @@ -1,6 +1,6 @@ request('get', '/', [ +$client->request('GET', '/', [ 'headers' => [ 'User-Agent' => 'testing/1.0', 'Accept' => 'application/json', diff --git a/source/libraries/images.rst b/source/libraries/images.rst index cdfb52fe..fc5c59a7 100755 --- a/source/libraries/images.rst +++ b/source/libraries/images.rst @@ -41,17 +41,19 @@ CodeIgniter 的图像处理类允许你执行以下操作: 处理图像 ******************* -无论你想执行的处理类型(调整大小、裁剪、旋转或添加水印),过程都是相同的。你将设置与你计划执行的操作相对应的一些首选项,然后调用可用的处理函数之一。例如,要创建图像缩略图,你将执行以下操作: +无论你想执行的处理类型(调整大小、裁剪、旋转或添加水印),过程都是相同的。你将设置与你计划执行的操作相对应的一些首选项,然后调用可用的处理函数之一。 + +例如,要创建图像缩略图,你将执行以下操作: .. literalinclude:: images/003.php -上面的代码告诉库查找名为 *mypic.jpg* 的位于 source_image 文件夹中的图像,然后使用 GD2 图像库根据所需的纵横比从中创建一个新的 100 x 100 像素的图像,并将其保存为一个新文件(缩略图)。由于它使用 ``fit()`` 方法,它将尝试根据所需的纵横比找到裁剪图像的最佳部分,然后裁剪并调整结果大小。 +上面的代码告诉库去寻找一个名为 **mypic.jpg** 的图像,该图像位于 **/path/to/image** 文件夹中,然后从中创建一个 100 x 100 像素的新图像,并将其保存到一个新文件 **mypic_thumb.jpg** 中。由于它使用了 ``fit()`` 方法,它将尝试根据所需的纵横比找到图像的最佳部分进行裁剪,然后裁剪并调整结果的大小。 在保存之前,可以通过尽可能多的可用方法对图像进行处理。原始图像保持不变,使用新的图像并通过每个方法传递,在之前的结果上叠加结果: .. literalinclude:: images/004.php -这个示例首先会修复任何移动手机方向问题,然后将图像旋转 90 度,然后从左上角开始裁剪结果为 100x100 像素的图像。结果将保存为缩略图。 +这个示例首先会修复任何移动手机方向问题,然后将图像旋转 90 度,然后从左上角开始裁剪结果为 100 x 100 像素的图像。结果将保存为缩略图。 .. note:: 为了允许图像类执行任何处理,包含图像文件的文件夹必须具有写入权限。 @@ -60,7 +62,7 @@ CodeIgniter 的图像处理类允许你执行以下操作: 图像质量 ============= -``save()`` 可以接受额外的参数 ``$quality`` 来更改结果图像的质量。值的范围从 0 到 100,默认值为 90。此参数仅适用于 JPEG 和 WEBP 图像,否则将被忽略: +``save()`` 可以接受额外的参数 ``$quality`` 来更改结果图像的质量。值的范围从 0 到 100,默认值为 90。此参数仅适用于 JPEG 和 WebP 图像,否则将被忽略: .. note:: 自 v4.4.0 起,WebP 格式可以使用 ``$quality`` 参数。 @@ -105,7 +107,7 @@ CodeIgniter 的图像处理类允许你执行以下操作: - ``$maintainRatio`` 如果为 true,将根据需要调整最终尺寸以维持图像的原始纵横比。 - ``$masterDim`` 指定在 ``$maintainRatio`` 为 true 时应保持不变的维度。值可以是: ``'width'``、 ``'height'`` 或 ``'auto'``。 -要从图像中心取一个 50x50 像素的正方形,你需要首先计算适当的 x 和 y 偏移值: +要从图像中心取一个 50 x 50 像素的正方形,你需要首先计算适当的 x 和 y 偏移值: .. literalinclude:: images/008.php @@ -213,16 +215,16 @@ CodeIgniter 的图像处理类允许你执行以下操作: 识别的可能选项如下: -- ``color`` 文本颜色(十六进制编号),例如 #ff0000 -- ``opacity`` 介于 0 和 1 之间表示文本不透明度的数字。 +- ``color`` 文本颜色(十六进制编号),例如 ``#ff0000`` +- ``opacity`` 介于 ``0`` 和 ``1`` 之间表示文本不透明度的数字。 - ``withShadow`` 布尔值,决定是否显示阴影。 - ``shadowColor`` 阴影的颜色(十六进制编号) - ``shadowOffset`` 阴影偏移多少像素。同时适用于垂直和水平值。 -- ``hAlign`` 水平对齐:左、中、右 -- ``vAlign`` 垂直对齐:顶部、中部、底部 +- ``hAlign`` 水平对齐: ``'left'``, ``'center'``, ``'right'`` +- ``vAlign`` 垂直对齐: ``'top'``, ``'middle'``, ``'bottom'`` - ``hOffset`` x 轴上的额外偏移,以像素为单位 - ``vOffset`` y 轴上的额外偏移,以像素为单位 - ``fontPath`` 你要使用的 TTF 字体的完整服务器路径。如果没有给出,则使用系统字体。 -- ``fontSize`` 要使用的字体大小。对于使用系统字体的 GD 处理程序,有效值为 1-5 之间。 +- ``fontSize`` 要使用的字体大小。对于使用系统字体的 GD 处理程序,有效值为 ``1`` 到 ``5`` 之间。 .. note:: ImageMagick 驱动程序不识别字体路径的完整服务器路径。相反,只提供你希望使用的已安装系统字体的名称,例如 Calibri。 diff --git a/source/libraries/index.rst b/source/libraries/index.rst index 60496964..a9685a81 100755 --- a/source/libraries/index.rst +++ b/source/libraries/index.rst @@ -7,6 +7,7 @@ caching cookies + cors curlrequest email encryption diff --git a/source/libraries/official_packages.rst b/source/libraries/official_packages.rst index 7aea614b..f38f922a 100644 --- a/source/libraries/official_packages.rst +++ b/source/libraries/official_packages.rst @@ -23,6 +23,8 @@ Shield * 每个用户的权限覆盖 * 等等... +.. _settings: + ******** Settings ******** diff --git a/source/libraries/security.rst b/source/libraries/security.rst index 5d20e301..36fe8f39 100755 --- a/source/libraries/security.rst +++ b/source/libraries/security.rst @@ -114,11 +114,13 @@ CSRF 保护方法 失败时重定向 ---------------------- -自 v4.3.0 起,当请求失败 CSRF 验证检查时,它将默认抛出 SecurityException。 +从 v4.5.0 开始,当请求未通过 CSRF 验证检查时,在生产环境中,默认情况下用户会被重定向到前一页面;在其他环境中,则会抛出一个 SecurityException。 .. note:: 在生产环境中,当你使用 HTML 表单时,建议启用此重定向以获得更好的用户体验。 -如果你想重定向到上一页,请在 **app/Config/Security.php** 中更改以下配置参数的值: + 升级用户应检查他们的配置文件。 + +如果你希望将其重定向到前一页面,请在 **app/Config/Security.php** 中将以下配置参数值设置为 ``true``: .. literalinclude:: security/005.php diff --git a/source/libraries/security/009.php b/source/libraries/security/009.php index bd6bc93c..fb9d114b 100644 --- a/source/libraries/security/009.php +++ b/source/libraries/security/009.php @@ -7,8 +7,8 @@ class Filters extends BaseConfig { public $methods = [ - 'get' => ['csrf'], - 'post' => ['csrf'], + 'GET' => ['csrf'], + 'POST' => ['csrf'], ]; // ... diff --git a/source/libraries/sessions.rst b/source/libraries/sessions.rst index a817c30f..5a131d55 100755 --- a/source/libraries/sessions.rst +++ b/source/libraries/sessions.rst @@ -512,7 +512,9 @@ DatabaseHandler 驱动程序 RedisHandler 驱动程序 ======================= -.. note:: 由于 Redis 没有公开锁定机制,因此通过单独保留 300 秒的额外值来模拟此驱动程序的锁。在 ``v4.3.2`` 或更高版本中,你可以使用 **TLS** 协议连接 ``Redis``。 +.. note:: 由于 Redis 没有暴露锁机制,因此该驱动的锁是通过一个单独的值来模拟的,该值最多保留 300 秒。 + +.. note:: 从 v4.3.2 开始,你可以使用 **TLS** 协议连接 Redis。 Redis 是一个通常用于缓存且以高性能而著称的存储引擎,这也可能是你使用 'RedisHandler' session 驱动程序的原因。 @@ -533,6 +535,12 @@ Redis 是一个通常用于缓存且以高性能而著称的存储引擎,这也 .. literalinclude:: sessions/041.php +从 v4.5.0 开始,你可以使用 Redis ACL(用户名和密码):: + + public string $savePath = 'tcp://localhost:6379?auth[user]=username&auth[pass]=password'; + +.. note:: 从 v4.5.0 开始,获取锁的间隔时间(``$lockRetryInterval``)和重试次数(``$lockMaxRetries``)是可配置的。 + .. _sessions-memcachedhandler-driver: MemcachedHandler 驱动程序 diff --git a/source/libraries/throttler/002.php b/source/libraries/throttler/002.php index 91c89ca8..440ad582 100644 --- a/source/libraries/throttler/002.php +++ b/source/libraries/throttler/002.php @@ -13,9 +13,9 @@ class Throttle implements FilterInterface * This is a demo implementation of using the Throttler class * to implement rate limiting for your application. * - * @param array|null $arguments + * @param list|null $arguments * - * @return mixed + * @return ResponseInterface|void */ public function before(RequestInterface $request, $arguments = null) { @@ -31,9 +31,9 @@ public function before(RequestInterface $request, $arguments = null) /** * We don't have anything to do here. * - * @param array|null $arguments + * @param list|null $arguments * - * @return mixed + * @return void */ public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { diff --git a/source/libraries/throttler/004.php b/source/libraries/throttler/004.php index ab4d9125..246b034d 100644 --- a/source/libraries/throttler/004.php +++ b/source/libraries/throttler/004.php @@ -7,7 +7,7 @@ class Filters extends BaseConfig { public $methods = [ - 'post' => ['throttle'], + 'POST' => ['throttle'], ]; // ... diff --git a/source/libraries/time.rst b/source/libraries/time.rst index eb654d04..d71fb861 100755 --- a/source/libraries/time.rst +++ b/source/libraries/time.rst @@ -4,7 +4,9 @@ CodeIgniter 提供了一个完全本地化的、不可变的日期/时间类,该类基于 PHP 的 DateTimeImmutable 对象构建,但使用 Intl 扩展的功能在时区之间转换时间并针对不同的语言环境正确显示输出。这个类是 ``Time`` 类,位于 ``CodeIgniter\I18n`` 命名空间中。 -.. note:: 由于 Time 类扩展了 ``DateTimeImmutable``,如果这个类没有提供你需要的功能,你很可能可以在 `DateTimeImmutable `_ 类本身中找到它们。 +.. note:: 由于 Time 类扩展了 ``DateTimeImmutable``,如果你需要的功能该类没有提供,你很可能可以在 `DateTimeImmutable`_ 类中找到它们。 + +.. _DateTimeImmutable: https://www.php.net/manual/zh/class.datetimeimmutable.php .. note:: 在 v4.3.0 之前,Time 类扩展了 ``DateTime``,并且一些继承的方法改变了当前对象状态。这个 bug 在 v4.3.0 中修复了。如果你需要旧的 Time 类用于向后兼容,你可以暂时使用已弃用的 ``TimeLegacy`` 类。 @@ -22,6 +24,10 @@ CodeIgniter 提供了一个完全本地化的、不可变的日期/时间类,该 .. _strtotime(): https://www.php.net/manual/en/function.strtotime.php +当你以这种方式执行时,你可以传入一个表示所需时间的字符串。这个字符串可以是 PHP 的 `DateTimeImmutable`_ 构造函数可以解析的任何字符串。详情请参见 `支持的日期和时间格式`_。 + +.. _支持的日期和时间格式: https://www.php.net/manual/zh/datetime.formats.php + .. literalinclude:: time/001.php 你可以在第二个和第三个参数中分别传递表示时区和语言环境的字符串。时区可以是 PHP 的 `DateTimeZone `__ 类支持的任何时区。语言环境可以是 PHP 的 `Locale `__ 类支持的任何语言环境。如果没有提供语言环境或时区,将使用应用程序默认值。 @@ -91,6 +97,8 @@ createFromFormat() .. literalinclude:: time/011.php +.. _time-createfromtimestamp: + createFromTimestamp() ===================== @@ -98,6 +106,8 @@ createFromTimestamp() .. literalinclude:: time/012.php +.. note:: 由于一个 bug,在 v4.4.6 之前的版本中,当你没有指定时区时,该方法返回的是 UTC 时区的 Time 实例。 + createFromInstance() ==================== @@ -310,10 +320,14 @@ isAfter() .. literalinclude:: time/037.php +.. _time-viewing-differences: + 查看差异 =================== -要直接比较两个 Time,你需要使用 ``difference()`` 方法,它返回一个 ``CodeIgniter\I18n\TimeDifference`` 实例。第一个参数是一个 Time 实例、一个 DateTime 实例或一个包含日期/时间的字符串。 如果在第一个参数中传递了一个字符串,第二个参数可以是一个时区字符串: +要直接比较两个 Time,你可以使用 ``difference()`` 方法,该方法返回一个 ``CodeIgniter\I18n\TimeDifference`` 实例。 + +第一个参数可以是一个 Time 实例、一个 DateTime 实例,或者一个包含日期/时间的字符串。如果第一个参数是字符串,第二个参数可以是一个时区字符串: .. literalinclude:: time/038.php @@ -321,6 +335,12 @@ isAfter() .. literalinclude:: time/039.php +.. note:: 在 v4.4.7 之前,Time 总是在比较之前将时区转换为 UTC。这可能会导致在由于夏令时 (DST) 导致一天不等于 24 小时时出现意外结果。 + + 从 v4.4.7 开始,当比较相同时区的日期/时间时,比较将按原样进行,不再转换为 UTC。 + + .. literalinclude:: time/042.php + 你可以使用 ``getX()`` 方法,也可以像访问属性一样访问计算的值: .. literalinclude:: time/040.php diff --git a/source/libraries/time/001.php b/source/libraries/time/001.php index ba7e4d3b..ccaf0741 100644 --- a/source/libraries/time/001.php +++ b/source/libraries/time/001.php @@ -2,5 +2,7 @@ use CodeIgniter\I18n\Time; -$myTime = new Time('+3 week'); +$myTime = new Time('2024-01-01'); +$myTime = new Time('2024-01-01 12:00:00'); $myTime = new Time('now'); +$myTime = new Time('+3 week'); diff --git a/source/libraries/time/042.php b/source/libraries/time/042.php new file mode 100644 index 00000000..6f2c7749 --- /dev/null +++ b/source/libraries/time/042.php @@ -0,0 +1,13 @@ +difference($test); + +echo $diff->getDays(); +// 0 in v4.4.6 or before +// 1 in v4.4.7 or later diff --git a/source/libraries/uploaded_files.rst b/source/libraries/uploaded_files.rst index 11568768..56810613 100755 --- a/source/libraries/uploaded_files.rst +++ b/source/libraries/uploaded_files.rst @@ -4,7 +4,7 @@ 在 CodeIgniter 中通过表单使用文件上传功能将会比直接使用 PHP 的 ``$_FILES`` 数组更加简单和安全。这是 :doc:`文件类 ` 的扩展,因此获得了该类所有的特性。 -.. note:: 这和 CodeIgniter v3.x 中的文件上传类不太一样。这里提供了一个访问上传文件的原始接口和一些小特性。 +.. note:: 这和 CodeIgniter 3 中的文件上传类不太一样。这里提供了一个访问上传文件的原始接口和一些小特性。 .. contents:: :local: diff --git a/source/libraries/uploaded_files/002.php b/source/libraries/uploaded_files/002.php index 44b33a85..af304c23 100644 --- a/source/libraries/uploaded_files/002.php +++ b/source/libraries/uploaded_files/002.php @@ -27,7 +27,7 @@ public function upload() ], ], ]; - if (! $this->validate($validationRule)) { + if (! $this->validateData([], $validationRule)) { $data = ['errors' => $this->validator->getErrors()]; return view('upload_form', $data); diff --git a/source/libraries/validation.rst b/source/libraries/validation.rst index fda6bb23..7e25086c 100755 --- a/source/libraries/validation.rst +++ b/source/libraries/validation.rst @@ -178,17 +178,26 @@ Form.php ============================ CodeIgniter 4 有两种验证规则类。 -传统规则类(**传统规则**)命名空间为 ``CodeIgniter\Validation``, -新的类(**严格规则**)命名空间为 ``CodeIgniter\Validation\StrictRules``,提供严格验证。 + +默认的规则类(**严格规则**)的命名空间是 ``CodeIgniter\Validation\StrictRules``,它们提供严格的验证。 + +传统的规则类(**传统规则**)的命名空间是 ``CodeIgniter\Validation``。它们仅为向后兼容而提供。它们可能无法正确验证非字符串值,不建议在新项目中使用。 .. note:: 从 v4.3.0 开始,默认使用 **严格规则** 以获得更好的安全性。 +严格规则 +------------ + +.. versionadded:: 4.2.0 + +**严格规则** 不使用隐式类型转换。 + 传统规则 ----------------- .. important:: 传统规则只存在于向后兼容性。在新项目中不要使用它们。即使你已经在使用它们,我们也建议切换到严格规则。 -.. warning:: 在验证包含非字符串值的数据时,比如 JSON 数据,推荐使用 **严格规则**。 +.. warning:: 在验证包含非字符串值的数据时,比如 JSON 数据,你应该使用 **严格规则**。 **传统规则** 隐式地假设正在验证字符串值,输入值可能会隐式转换为字符串值。 它适用于大多数基本情况,如验证 POST 数据。 @@ -197,16 +206,11 @@ CodeIgniter 4 有两种验证规则类。 当你使用传统规则类验证布尔值 ``true`` 时,它会被转换为字符串 ``'1'``。 如果使用 ``integer`` 规则对其进行验证, ``'1'`` 可以通过验证。 -严格规则 ------------- - -.. versionadded:: 4.2.0 - -**严格规则** 不使用隐式类型转换。 - 使用传统规则 ----------------------- +.. warning:: **传统规则** 仅为向后兼容而提供。它们可能无法正确验证非字符串值,不建议在新项目中使用。 + 如果你想使用传统规则,需要在 **app/Config/Validation.php** 中更改规则类: .. literalinclude:: validation/003.php @@ -225,6 +229,17 @@ CodeIgniter 4 有两种验证规则类。 .. note:: 你可能永远不需要使用这个方法,因为 :doc:`控制器 ` 和 :doc:`模型 ` 都提供了使验证更容易的方法。 +******************** +验证的工作原理 +******************** + +- 验证从不改变要验证的数据。 +- 验证会根据你设置的验证规则依次检查每个字段。如果任何规则返回 false,则该字段的检查到此结束。 +- 格式规则不允许空字符串。如果你想允许空字符串,请添加 ``permit_empty`` 规则。 +- 如果要验证的数据中不存在某个字段,则该值被解释为 ``null``。如果你想检查该字段是否存在,请添加 ``field_exists`` 规则。 + +.. note:: ``field_exists`` 规则自 v4.5.0 起可用。 + ************************ 设置验证规则 ************************ @@ -434,12 +449,14 @@ withRequest() 验证占位符 ======================= -验证类提供了一种简单的方法,基于传入的数据替换规则的一部分。这听起来比较模糊,但在使用 ``is_unique`` 验证规则时特别有用。占位符简单地是以花括号括起来的字段名(或数组键),该字段名作为 ``$data`` 传入。它将被传入字段的 **值** 所替换。以下示例将阐明这一点: +验证类提供了一种简单的方法,基于传入的数据替换规则的一部分。这听起来比较模糊,但在使用 ``is_unique`` 验证规则时特别有用。 + +占位符简单地是以花括号括起来的字段名(或数组键),该字段名作为 ``$data`` 传入。它将被传入字段的 **值** 所替换。以下示例将阐明这一点: .. literalinclude:: validation/020.php :lines: 2- -.. note:: 从 v4.3.5 开始,出于安全考虑,你必须为占位符字段(上面示例代码中的 ``id`` 字段)设置验证规则。 +.. warning:: 自 v4.3.5 起,出于安全原因,你必须为占位符字段(上述示例代码中的 ``id`` 字段)设置验证规则。因为攻击者可以向你的应用程序发送任何数据。 在这组规则中,它说明电子邮件地址在数据库中应该是唯一的,除了具有与占位符的值匹配的 id 的行。假设表单 POST 数据如下: @@ -698,6 +715,29 @@ PHP 请求之间不共享任何内容。所以在验证失败时重定向,重定 .. literalinclude:: validation/041.php :lines: 2- +.. _validation-using-callable-rule: + +使用可调用规则 +=================== + +.. versionadded:: 4.5.0 + +如果你想使用数组回调作为规则,可以用它来代替闭包规则。 + +你需要为验证规则使用数组: + +.. literalinclude:: validation/046.php + :lines: 2- + +你必须为可调用规则设置错误消息。 +当你指定错误消息时,为可调用规则设置数组键。 +在上面的代码中,``required`` 规则的键是 ``0``,而可调用规则的键是 ``1``。 + +或者你可以使用以下参数: + +.. literalinclude:: validation/047.php + :lines: 2- + *************** 可用规则 *************** @@ -734,6 +774,7 @@ decimal 无 如果字段包含除十进制数字外的 也接受数字前的 ``+`` 或 ``-`` 号。 differs 是 如果字段与参数中字段的值不不同,则失败。 ``differs[field_name]`` exact_length 是 如果字段的长度不完全等于参数值,则失败。 ``exact_length[5]`` 或 ``exact_length[5,8,12]`` +field_exists 是 如果字段不存在则失败。(此规则在 v4.5.0 中添加。) greater_than 是 如果字段小于或等于参数值,或不是数字,则失败。 ``greater_than[8]`` greater_than_equal_to 是 如果字段小于参数值,或不是数字,则失败。 ``greater_than_equal_to[5]`` hex 无 如果字段包含除十六进制字符之外的任何内容,则失败。 @@ -819,7 +860,7 @@ valid_cc_number 是 验证信用卡号是否与指定提供程 // 在控制器中 - $this->validate([ + $this->validateData([], [ 'avatar' => 'uploaded[avatar]|max_size[avatar,1024]', ]); diff --git a/source/libraries/validation/045.php b/source/libraries/validation/045.php index 1becb578..401897f0 100644 --- a/source/libraries/validation/045.php +++ b/source/libraries/validation/045.php @@ -2,7 +2,7 @@ // In Controller. -if (! $this->validate([ +if (! $this->validateData($data, [ 'username' => 'required', 'password' => 'required|min_length[10]', ])) { diff --git a/source/libraries/validation/046.php b/source/libraries/validation/046.php new file mode 100644 index 00000000..f37def00 --- /dev/null +++ b/source/libraries/validation/046.php @@ -0,0 +1,43 @@ +setRules( + [ + 'foo' => [ + 'required', + // Specify the method in this controller as a rule. + [$this, '_ruleEven'], + ], + ], + [ + // Errors + 'foo' => [ + // Specify the array key for the callable rule. + 1 => 'The value is not even.', + ], + ], + ); + + if (! $validation->run($data)) { + // handle validation errors + } + + // ... + } +} diff --git a/source/libraries/validation/047.php b/source/libraries/validation/047.php new file mode 100644 index 00000000..6c2bb674 --- /dev/null +++ b/source/libraries/validation/047.php @@ -0,0 +1,22 @@ + 'base64', ]; diff --git a/source/models/entities/020.php b/source/models/entities/020.php index 7d3708b0..bedf6d12 100644 --- a/source/models/entities/020.php +++ b/source/models/entities/020.php @@ -6,7 +6,7 @@ class MyEntity extends Entity { - // Defining a type with parameters + // Define a type with parameters protected $casts = [ 'some_attribute' => 'class[App\SomeClass, param2, param3]', ]; diff --git a/source/models/model.rst b/source/models/model.rst index d9e31d23..572c7cd7 100644 --- a/source/models/model.rst +++ b/source/models/model.rst @@ -97,7 +97,11 @@ $useAutoIncrement $returnType ----------- -模型的 CRUD 方法将一个步骤的工作从你这里带走,自动返回结果数据,而不是结果对象。此设置允许你定义返回的数据类型。有效值为 '**array**' (默认)、 '**object**' 或可以与结果对象的 ``getCustomResultObject()`` 方法一起使用的 **完全限定的类名称**。使用类的特殊常量 ``::class`` 可以让大多数 IDE 自动完成名称并允许诸如重构之类的功能更好地理解你的代码。 +模型的 **find*()** 方法将为你减少一些工作,自动返回结果数据,而不是 Result 对象。 + +此设置允许你定义返回的数据类型。有效值为 '**array**'(默认值)、'**object**' 或者可以与 Result 对象的 ``getCustomResultObject()`` 方法一起使用的 **类的完全限定名**。 + +使用类的特殊常量 ``::class`` 将允许大多数 IDE 自动补全名称,并使重构等功能更好地理解你的代码。 .. _model-use-soft-deletes: @@ -108,7 +112,9 @@ $useSoftDeletes 这需要数据库中具有与模型的 `$dateFormat`_ 设置相应的数据类型的 DATETIME 或 INTEGER 字段。默认字段名称为 ``deleted_at``,但是可以通过使用 `$deletedField`_ 属性将其配置为你选择的任何名称。 -.. important:: ``deleted_at`` 字段必须可为空。 +.. important:: 数据库中的 ``deleted_at`` 字段必须是 nullable 的。 + +.. _model-allowed-fields: $allowedFields -------------- @@ -122,10 +128,29 @@ $allowEmptyInserts .. versionadded:: 4.3.0 -是否允许插入空数据。默认值是 ``false``,意味着如果你试图插入空数据,将会抛出 "There is no data to insert." 的异常。 +是否允许插入空数据。默认值是 ``false``,这意味着如果你尝试插入空数据,将会抛出带有 "There is no data to insert." 信息的 ``DataException``。 你也可以通过 :ref:`model-allow-empty-inserts` 方法来改变这个设置。 +.. _model-update-only-changed: + +$updateOnlyChanged +------------------ + +.. versionadded:: 4.5.0 + +是否仅更新 :doc:`Entity <./entities>` 的已更改字段。默认值是 ``true``,这意味着在更新到数据库时仅使用已更改的字段数据。因此,如果你尝试更新一个没有更改的 Entity,将会抛出带有 "There is no data to update." 信息的 ``DataException``。 + +将此属性设置为 ``false`` 将确保 Entity 的所有允许字段在任何时候都提交到数据库并进行更新。 + +$casts +------ + +.. versionadded:: 4.5.0 + +这允许你将从数据库检索到的数据转换为适当的 PHP 类型。 +此选项应为一个数组,其中键是字段的名称,值是数据类型。详情请参见 :ref:`model-field-casting`。 + 日期 ----- @@ -137,7 +162,7 @@ $useTimestamps $dateFormat ^^^^^^^^^^^ -此值与 `$useTimestamps`_ 和 `$useSoftDeletes`_ 一起使用,以确保插入到数据库中的是正确类型的日期值。默认情况下,这会创建 DATETIME 值,但有效选项有: ``'datetime'``、 ``'date'`` 或 ``'int'`` (PHP 时间戳)。在缺少或无效的 `$dateFormat`_ 情况下使用 `$useSoftDeletes`_ 或 `$useTimestamps`_ 会引发异常。 +此值与 `$useTimestamps`_ 和 `$useSoftDeletes`_ 一起使用,以确保插入到数据库中的是正确类型的日期值。默认情况下,这会创建 DATETIME 值,但有效选项有: ``'datetime'``、 ``'date'`` 或 ``'int'`` (UNIX 时间戳)。在缺少或无效的 `$dateFormat`_ 情况下使用 `$useSoftDeletes`_ 或 `$useTimestamps`_ 会引发异常。 $createdField ^^^^^^^^^^^^^ @@ -220,6 +245,112 @@ $afterUpdateBatch 这些数组允许你指定在属性名称中指定的时间回调方法。参考 :ref:`model-events`。 +.. _model-field-casting: + +模型字段类型转换 +**************** + +.. versionadded:: 4.5.0 + +从数据库检索数据时,整数类型的数据可能会在 PHP 中转换为字符串类型。你可能还希望将日期/时间数据转换为 PHP 中的 Time 对象。 + +模型字段类型转换允许你将从数据库检索到的数据转换为适当的 PHP 类型。 + +.. important:: + 如果你在使用 :doc:`Entity <./entities>` 时使用了此功能,请不要使用 + :ref:`Entity 属性类型转换 `。同时使用这两种类型转换是无效的。 + + Entity 属性类型转换在 (1)(4) 处工作,而此类型转换在 (2)(3) 处工作:: + + [应用代码] --- (1) --> [Entity] --- (2) --> [数据库] + [应用代码] <-- (4) --- [Entity] <-- (3) --- [数据库] + + 使用此类型转换时,Entity 将在属性中具有正确类型的 PHP 值。这种行为与之前的行为完全不同。不要期望属性中持有来自数据库的原始数据。 + +定义数据类型 +============= + +``$casts`` 属性设置其定义。此选项应为一个数组,其中键是字段的名称,值是数据类型: + +.. literalinclude:: model/057.php + +数据类型 +========= + +默认提供以下类型。在类型前添加问号以标记字段为可为空,例如,``?int``,``?datetime``。 + ++---------------+----------------+---------------------------+ +| 类型 | PHP 类型 | 数据库字段类型 | ++===============+================+===========================+ +|``int`` | int | int 类型 | ++---------------+----------------+---------------------------+ +|``float`` | float | float(数值)类型 | ++---------------+----------------+---------------------------+ +|``bool`` | bool | bool/int/string 类型 | ++---------------+----------------+---------------------------+ +|``int-bool`` | bool | int 类型(1 或 0) | ++---------------+----------------+---------------------------+ +|``array`` | array | string 类型(序列化) | ++---------------+----------------+---------------------------+ +|``csv`` | array | string 类型(CSV) | ++---------------+----------------+---------------------------+ +|``json`` | stdClass | json/string 类型 | ++---------------+----------------+---------------------------+ +|``json-array`` | array | json/string 类型 | ++---------------+----------------+---------------------------+ +|``datetime`` | Time | datetime 类型 | ++---------------+----------------+---------------------------+ +|``timestamp`` | Time | int 类型(UNIX 时间戳) | ++---------------+----------------+---------------------------+ +|``uri`` | URI | string 类型 | ++---------------+----------------+---------------------------+ + +csv +--- + +将类型转换为 ``csv`` 使用 PHP 的内部 ``implode()`` 和 ``explode()`` 函数,并假设所有值都是字符串安全且不含逗号。对于更复杂的数据类型转换,尝试使用 ``array`` 或 ``json``。 + +datetime +-------- + +你可以传递类似 ``datetime[ms]`` 的参数表示带有毫秒的日期/时间,或 ``datetime[us]`` 表示带有微秒的日期/时间。 + +日期时间格式在 **app/Config/Database.php** 文件中的 :ref:`数据库配置 ` 的 ``dateFormat`` 数组中设置。 + +自定义类型转换 +=============== + +你可以定义自己的转换类型。 + +创建自定义处理程序 +-------------------- + +首先,你需要为你的类型创建一个处理程序类。假设该类位于 **app/Models/Cast** 目录中: + +.. literalinclude:: model/058.php + +如果你不需要在获取或设置值时更改值,那么只需不实现相应的方法: + +.. literalinclude:: model/060.php + +注册自定义处理程序 +-------------------- + +现在你需要注册它: + +.. literalinclude:: model/059.php + +参数 +---- + +在某些情况下,一种类型是不够的。在这种情况下,你可以使用附加参数。附加参数在方括号中指示,并用逗号列出,例如 ``type[param1, param2]``。 + +.. literalinclude:: model/061.php + +.. literalinclude:: model/062.php + +.. note:: 如果类型转换类型标记为 nullable,例如 ``?bool``,并且传递的值不为空,那么值为 ``nullable`` 的参数将传递给类型转换处理程序。如果类型转换具有预定义参数,则 ``nullable`` 将添加到列表的末尾。 + 使用数据 ***************** @@ -365,6 +496,18 @@ save 方法在使用自定义类结果对象时也可以大大简化工作,它 .. note:: 如果你发现自己频繁使用实体,CodeIgniter 提供了一个内置的 :doc:`实体类 `, 它提供了使开发实体更简单的几个方便的功能。 +.. _model-saving-dates: + +保存日期 +-------- + +.. versionadded:: 4.5.0 + +在保存数据时,如果你传递 :doc:`Time <../libraries/time>` 实例,它们会根据 +:ref:`数据库配置 ` 中 ``dateFormat['datetime']`` 和 ``dateFormat['date']`` 定义的格式转换为字符串。 + +.. note:: 在 v4.5.0 之前,日期/时间格式在 Model 类中是硬编码为 ``Y-m-d H:i:s`` 和 ``Y-m-d``。 + 删除数据 ============= diff --git a/source/models/model/003.php b/source/models/model/003.php index 1b70cea8..c2ef0d25 100644 --- a/source/models/model/003.php +++ b/source/models/model/003.php @@ -6,6 +6,8 @@ class UserModel extends UserAuthModel { + // ... + /** * Called during initialization. Appends * our custom field to the module's model. diff --git a/source/models/model/004.php b/source/models/model/004.php index 1ae028ac..1005e6f7 100644 --- a/source/models/model/004.php +++ b/source/models/model/004.php @@ -7,4 +7,6 @@ class UserModel extends Model { protected $DBGroup = 'group_name'; + + // ... } diff --git a/source/models/model/005.php b/source/models/model/005.php index 956870af..5e437a3f 100644 --- a/source/models/model/005.php +++ b/source/models/model/005.php @@ -17,6 +17,7 @@ class UserModel extends Model protected $allowedFields = ['name', 'email']; protected bool $allowEmptyInserts = false; + protected bool $updateOnlyChanged = true; // Dates protected $useTimestamps = false; diff --git a/source/models/model/027.php b/source/models/model/027.php index 3a039df4..86a5a5f4 100644 --- a/source/models/model/027.php +++ b/source/models/model/027.php @@ -6,6 +6,8 @@ class UserModel extends Model { + // ... + protected $validationRules = [ 'username' => 'required|max_length[30]|alpha_numeric_space|min_length[3]', 'email' => 'required|max_length[254]|valid_email|is_unique[users.email]', diff --git a/source/models/model/034.php b/source/models/model/034.php index b18544ee..ac898926 100644 --- a/source/models/model/034.php +++ b/source/models/model/034.php @@ -6,5 +6,7 @@ class UserModel extends Model { + // ... + protected $validationRules = 'users'; } diff --git a/source/models/model/038.php b/source/models/model/038.php index 5ed40baa..2e61e1fc 100644 --- a/source/models/model/038.php +++ b/source/models/model/038.php @@ -6,6 +6,8 @@ class MyModel extends Model { + // ... + protected $validationRules = [ 'id' => 'max_length[19]|is_natural_no_zero', 'email' => 'required|max_length[254]|valid_email|is_unique[users.email,id,{id}]', diff --git a/source/models/model/040.php b/source/models/model/040.php index df4d628d..94de0d04 100644 --- a/source/models/model/040.php +++ b/source/models/model/040.php @@ -6,6 +6,8 @@ class MyModel extends Model { + // ... + protected $validationRules = [ 'id' => 'max_length[19]|is_natural_no_zero', 'email' => 'required|max_length[254]|valid_email|is_unique[users.email,id,4]', diff --git a/source/models/model/041.php b/source/models/model/041.php index 94ad48e0..d5b73ec3 100644 --- a/source/models/model/041.php +++ b/source/models/model/041.php @@ -6,5 +6,7 @@ class MyModel extends Model { + // ... + protected $allowedFields = ['name', 'email', 'address']; } diff --git a/source/models/model/050.php b/source/models/model/050.php index 9f6d3346..5f5a4fdb 100644 --- a/source/models/model/050.php +++ b/source/models/model/050.php @@ -6,6 +6,8 @@ class MyModel extends Model { + // ... + protected function hashPassword(array $data) { if (! isset($data['data']['password'])) { diff --git a/source/models/model/051.php b/source/models/model/051.php index 0ca5ee31..28229e22 100644 --- a/source/models/model/051.php +++ b/source/models/model/051.php @@ -6,6 +6,10 @@ class MyModel extends Model { + // ... + protected $beforeInsert = ['hashPassword']; protected $beforeUpdate = ['hashPassword']; + + // ... } diff --git a/source/models/model/052.php b/source/models/model/052.php index 292b752b..d049e938 100644 --- a/source/models/model/052.php +++ b/source/models/model/052.php @@ -6,5 +6,9 @@ class MyModel extends Model { + // ... + protected $allowCallbacks = false; + + // ... } diff --git a/source/models/model/054.php b/source/models/model/054.php index 6b72095f..c67b4828 100644 --- a/source/models/model/054.php +++ b/source/models/model/054.php @@ -6,6 +6,8 @@ class MyModel extends Model { + // ... + protected $beforeFind = ['checkCache']; // ... diff --git a/source/models/model/057.php b/source/models/model/057.php new file mode 100644 index 00000000..6b7ca325 --- /dev/null +++ b/source/models/model/057.php @@ -0,0 +1,17 @@ + 'int', + 'birthdate' => '?datetime', + 'hobbies' => 'json-array', + 'active' => 'int-bool', + ]; + // ... +} diff --git a/source/models/model/058.php b/source/models/model/058.php new file mode 100644 index 00000000..35ba857f --- /dev/null +++ b/source/models/model/058.php @@ -0,0 +1,40 @@ + 'base64', + ]; + + // Bind the type to the handler + protected array $castHandlers = [ + 'base64' => CastBase64::class, + ]; + + // ... +} diff --git a/source/models/model/060.php b/source/models/model/060.php new file mode 100644 index 00000000..7405f894 --- /dev/null +++ b/source/models/model/060.php @@ -0,0 +1,27 @@ + 'class[App\SomeClass, param2, param3]', + ]; + + // Bind the type to the handler + protected array $castHandlers = [ + 'class' => SomeHandler::class, + ]; + + // ... +} diff --git a/source/models/model/062.php b/source/models/model/062.php new file mode 100644 index 00000000..ebf86828 --- /dev/null +++ b/source/models/model/062.php @@ -0,0 +1,27 @@ + + * string(13) "App\SomeClass" + * [1]=> + * string(6) "param2" + * [2]=> + * string(6) "param3" + * } + */ + } +} diff --git a/source/outgoing/alternative_php.rst b/source/outgoing/alternative_php.rst index 9c71a803..47371689 100755 --- a/source/outgoing/alternative_php.rst +++ b/source/outgoing/alternative_php.rst @@ -11,11 +11,11 @@ 通常要输出或打印一个变量,你会这样做:: - + 使用替代语法,你可以这样做:: - + 替代控制结构 ============================== diff --git a/source/outgoing/alternative_php/001.php b/source/outgoing/alternative_php/001.php index 9158fe27..b9c543ef 100644 --- a/source/outgoing/alternative_php/001.php +++ b/source/outgoing/alternative_php/001.php @@ -2,7 +2,7 @@ -

  • +
  • diff --git a/source/outgoing/api_responses.rst b/source/outgoing/api_responses.rst index 7663e067..4d7048fd 100755 --- a/source/outgoing/api_responses.rst +++ b/source/outgoing/api_responses.rst @@ -24,19 +24,20 @@ API 响应特性 .. literalinclude:: api_responses/002.php +.. _api-response-trait-handling-response-types: + *********************** 处理响应类型 *********************** 当你在任何这些方法中传递数据时,它们将根据以下标准确定数据类型以格式化结果: -* 如果数据是字符串,它将被视为要返回给客户端的 HTML。 -* 如果数据是数组,它将根据控制器的 ``$this->format`` 值进行格式化。如果为空, - 它将尝试用客户端请求的内容类型协商内容类型,默认为 JSON - 如果在 **Config/Format.php** 中的 ``$supportedResponseFormats`` 属性未指定其他格式。 +* 格式是根据控制器的 ``$this->format`` 值确定的。如果该值为 ``null``,它将尝试与客户端请求的内容类型进行协商,默认为 **app/Config/Format.php** 中 ``$supportedResponseFormats`` 属性的第一个元素(默认是 JSON)。 +* 数据将根据格式进行格式化。如果格式不是 JSON 且数据是字符串,它将被视为 HTML 发送回客户端。 + +.. note:: 在 v4.5.0 之前,由于一个错误,如果数据是字符串,即使格式是 JSON,它也会被视为 HTML。 -要定义用于格式化的格式器,请编辑 **Config/Format.php**。 ``$supportedResponseFormats`` 包含应用程序可以 -自动格式化响应的 mime 类型列表。默认情况下,系统知道如何格式化 XML 和 JSON 响应: +要定义使用的格式化程序,请编辑 **app/Config/Format.php**。``$supportedResponseFormats`` 包含你的应用程序可以自动格式化响应的 mime 类型列表。默认情况下,系统知道如何格式化 XML 和 JSON 响应: .. literalinclude:: api_responses/003.php diff --git a/source/outgoing/csp.rst b/source/outgoing/csp.rst index 5765842e..e791f8ac 100644 --- a/source/outgoing/csp.rst +++ b/source/outgoing/csp.rst @@ -43,16 +43,32 @@ 运行时配置 ********************* -如果你的应用程序需要在运行时进行更改,你可以在你的控制器中通过 ``$this->response->getCSP()`` 访问实例。这个类包含了一些方法,这些方法与你需要设置的适当的头部值相当明显的映射。下面展示了一些例子,它们有不同的参数组合,尽管所有的都接受一个指令名或者它们的数组: +如果你的应用程序需要在运行时进行更改,你可以在你的控制器中通过 ``$this->response->getCSP()`` 访问实例。 + +这个类包含了一些方法,这些方法与你需要设置的适当的头部值相当明显的映射。下面展示了一些例子,它们有不同的参数组合,尽管所有的都接受一个指令名或者它们的数组: .. literalinclude:: csp/012.php 每个 "add" 方法的第一个参数是一个适当的字符串值,或者是它们的数组。 -``reportOnly()`` 方法允许你为后续的源指定默认的报告处理,除非被覆盖。例如,你可以指定 youtube.com 是被允许的,然后提供几个被允许但被报告的源: +仅报告 +======= + +``reportOnly()`` 方法允许你为后续的资源指定默认的报告处理方式,除非被覆盖。 + +例如,你可以指定 youtube.com 被允许,然后提供几个允许但需要报告的资源: .. literalinclude:: csp/013.php +.. _csp-clear-directives: + +清除指令 +======== + +如果你想清除现有的 CSP 指令,可以使用 ``clearDirective()`` 方法: + +.. literalinclude:: csp/014.php + ************** 内联内容 ************** diff --git a/source/outgoing/csp/014.php b/source/outgoing/csp/014.php new file mode 100644 index 00000000..ced4a930 --- /dev/null +++ b/source/outgoing/csp/014.php @@ -0,0 +1,6 @@ +response->getCSP(); + +$csp->clearDirective('style-src'); diff --git a/source/outgoing/localization.rst b/source/outgoing/localization.rst index f00e6463..e5787017 100644 --- a/source/outgoing/localization.rst +++ b/source/outgoing/localization.rst @@ -7,7 +7,7 @@ :depth: 3 ******************** -使用区域设置 +使用语言环境 ******************** CodeIgniter 提供了几个工具来帮助你为不同的语言本地化应用程序。尽管完整地本地化一个应用程序是个复杂的题目,但在应用程序中用不同的支持语言交换字符串是非常简单的。 @@ -24,7 +24,7 @@ CodeIgniter 提供了几个工具来帮助你为不同的语言本地化应用 .. important:: 仅基于网络的请求才会启用本地化检测,需要使用 IncomingRequest 类。 命令行下的请求不支持这些特性。 -配置区域设置 +配置语言环境 ====================== .. _setting-the-default-locale: @@ -40,23 +40,23 @@ CodeIgniter 提供了几个工具来帮助你为不同的语言本地化应用 如果无法找到完全匹配的语言环境,系统足够智能到退回到更通用的语言代码。如果语言环境代码被设置为 ``en-US``,但我们只设置了 ``en`` 的语言文件,那么就会使用 ``en``,因为没有更具体的 ``en-US`` 存在。然而,如果 **app/Language/en-US** 目录存在,那么它将被首先使用。 -区域设置检测 +语言环境检测 ================ -在请求期间支持两种方法来检测正确的区域设置。第一种方法是一个“设置完就忘记”的方式,将自动为你执行 :doc:`内容协商 ` 以确定使用的正确区域设置。第二种方法允许你在路由中指定一个片段来设置区域设置。 +在请求期间支持两种方法来检测正确的语言环境。第一种方法是一个“设置完就忘记”的方式,将自动为你执行 :doc:`内容协商 ` 以确定使用的正确语言环境。第二种方法允许你在路由中指定一个片段来设置语言环境。 -如果你需要直接设置区域设置,请参见 `设置当前区域设置`_ 。 +如果你需要直接设置语言环境,请参见 `设置当前语言环境`_ 。 -自 v4.4.0 起,添加了 ``IncomingRequest::setValidLocales()`` 方法,用于设置(和重置)从 ``Config\App::$supportedLocales`` 设置中设置的有效区域设置。 +自 v4.4.0 起,添加了 ``IncomingRequest::setValidLocales()`` 方法,用于设置(和重置)从 ``Config\App::$supportedLocales`` 设置中设置的有效语言环境。 内容协商 ------------------- -你可以通过在 **app/Config/App.php** 中设置两个附加配置来启用内容协商自动执行。第一个值告诉 Request 类我们确实希望协商一个区域设置,所以简单地设置为 true : +你可以通过在 **app/Config/App.php** 中设置两个附加配置来启用内容协商自动执行。第一个值告诉 Request 类我们确实希望协商一个语言环境,所以简单地设置为 true : .. literalinclude:: localization/002.php -一旦启用,系统将自动根据你定义的区域设置列表 ``$supportLocales`` 协商出正确的语言。如果在你支持的语言和请求的语言之间找不到匹配, ``$supportedLocales`` 中的第一项将被使用。在下面的例子中,如果找不到匹配,将使用 ``en`` 区域设置: +一旦启用,系统将自动根据你定义的语言环境列表 ``$supportLocales`` 协商出正确的语言。如果在你支持的语言和请求的语言之间找不到匹配, ``$supportedLocales`` 中的第一项将被使用。在下面的例子中,如果找不到匹配,将使用 ``en`` 语言环境: .. literalinclude:: localization/003.php @@ -65,33 +65,35 @@ CodeIgniter 提供了几个工具来帮助你为不同的语言本地化应用 在路由中 --------- -第二种方法使用一个自定义占位符来检测所需的区域设置并在 Request 中设置它。可以将占位符 ``{locale}`` 作为路由中的一个片段。如果存在,匹配片段的内容将是你的区域设置: +第二种方法使用一个自定义占位符来检测所需的语言环境并在 Request 中设置它。可以将占位符 ``{locale}`` 作为路由中的一个片段。如果存在,匹配片段的内容将是你的语言环境: .. literalinclude:: localization/004.php -在这个例子中,如果用户试图访问 **http://example.com/fr/books**,那么区域设置将被设置为 ``fr``,假设它已经在 ``$supportedLocales`` 中被配置为有效的区域设置。 +在这个例子中,如果用户试图访问 **http://example.com/fr/books**,那么语言环境将被设置为 ``fr``,假设它已经在 ``$supportedLocales`` 中被配置为有效的语言环境。 -如果该值与 ``app/Config/App.php`` 中定义的 ``$supportedLocales`` 中的有效区域设置不匹配,将使用默认区域设置取代,除非你设置只使用 App 配置文件中定义的受支持区域设置: +如果该值与 ``app/Config/App.php`` 中定义的 ``$supportedLocales`` 中的有效语言环境不匹配,将使用默认语言环境取代,除非你设置只使用 App 配置文件中定义的受支持语言环境: .. literalinclude:: localization/018.php .. note:: ``useSupportedLocalesOnly()`` 方法可以在 v4.3.0 及以上版本中使用。 -设置当前区域设置 +设置当前语言环境 ========================== -如果你想直接设置区域设置,可以使用 ``IncomingRequest::setLocale(string $locale)``。 -你必须在 **app/Config/App.php** 中设置支持的区域设置: +如果你想直接设置语言环境,可以使用 ``IncomingRequest::setLocale(string $locale)``。 + +在设置语言环境之前,你必须设置有效的语言环境。因为任何尝试设置无效语言环境的操作都会导致设置 :ref:`默认语言环境 `。 + +默认情况下,有效的语言环境在 **app/Config/App.php** 中的 ``Config\App::$supportedLocales`` 中定义: .. literalinclude:: localization/003.php -.. note:: 尝试设置一个不在该数组中的区域设置的任何尝试都将结果在 - :ref:`默认区域设置 ` 被设置。 +.. note:: 自 v4.4.0 起,``IncomingRequest::setValidLocales()`` 已被添加用于设置(和重置)有效的语言环境。如果你想动态更改有效的语言环境,请使用它。 -获取当前区域设置 +获取当前语言环境 ============================= -当前区域设置始终可以从 IncomingRequest 对象中通过 ``getLocale()`` 方法获取。 +当前语言环境始终可以从 IncomingRequest 对象中通过 ``getLocale()`` 方法获取。 如果你的控制器继承了 ``CodeIgniter\Controller``,它将通过 ``$this->request`` 可用: .. literalinclude:: localization/005.php @@ -132,7 +134,7 @@ CodeIgniter 提供了几个工具来帮助你为不同的语言本地化应用 .. literalinclude:: localization/011.php -如果请求的语言键在当前区域设置的文件中不存在,字符串将原封不动地返回。在此例中,如果它不存在,将返回 'Errors.errorEmailMissing' 或者 'Errors.nested.error.message'。 +如果请求的语言键在当前语言环境的文件中不存在,字符串将原封不动地返回。在此例中,如果它不存在,将返回 'Errors.errorEmailMissing' 或者 'Errors.nested.error.message'。 替换参数 -------------------- @@ -168,10 +170,10 @@ CodeIgniter 提供了几个工具来帮助你为不同的语言本地化应用 你应该确保阅读 MessageFormatter 类和基础 ICU 格式相关的内容,以更好地了解它的功能,比如执行条件替换、复数化等。之前提供的两个链接都会给你关于可用选项的极好的主意。 -指定区域设置 +指定语言环境 ----------------- -要指定不同的区域设置用于替换参数,你可以将区域设置作为第三个参数传递给 ``lang()`` 函数。 +要指定不同的语言环境用于替换参数,你可以将语言环境作为第三个参数传递给 ``lang()`` 函数。 .. literalinclude:: localization/016.php @@ -185,17 +187,17 @@ CodeIgniter 提供了几个工具来帮助你为不同的语言本地化应用 语言回退 ================= -如果你为一个给定的区域设置拥有一组消息,例如 -**Language/en/app.php**,你可以为该区域设置添加语言变体, +如果你为一个给定的语言环境拥有一组消息,例如 +**Language/en/app.php**,你可以为该语言环境添加语言变体, 每种变体各自一个文件夹,例如 **Language/en-US/app.php**。 -你只需要为需要针对该区域设置变体进行本地化的消息提供不同的值。 -任何缺失的消息定义将自动从主要的区域设置中获取。 +你只需要为需要针对该语言环境变体进行本地化的消息提供不同的值。 +任何缺失的消息定义将自动从主要的语言环境中获取。 更好的是,本地化可以一直回退到英语, -以防还没有机会为你的区域设置翻译框架中添加的新消息。 +以防还没有机会为你的语言环境翻译框架中添加的新消息。 -因此,如果你使用 ``fr-CA`` 区域设置,则本地化消息将首先在 +因此,如果你使用 ``fr-CA`` 语言环境,则本地化消息将首先在 **Language/fr-CA** 目录中查找,然后在 **Language/fr** 目录中查找, 最后在 **Language/en** 目录中查找。 @@ -214,3 +216,61 @@ CodeIgniter 提供了几个工具来帮助你为不同的语言本地化应用 composer require codeigniter4/translations 翻译的消息将自动被使用,因为翻译文件夹得到了正确的映射。 + +.. _generating-translation-files-via-command: + +通过命令生成翻译文件 +==================== + +.. versionadded:: 4.5.0 + +你可以在 **app** 文件夹中自动生成和更新翻译文件。该命令将搜索 ``lang()`` 函数的使用,通过定义 ``Config\App`` 中的语言环境 ``defaultLocale`` 来合并 **app/Language** 中当前的翻译键。操作完成后,你需要自行翻译这些语言键。该命令能够正常识别嵌套键,例如 ``File.array.nested.text``。之前保存的键不会改变。 + +.. code-block:: console + + php spark lang:find + +.. literalinclude:: localization/019.php + +.. note:: 当命令扫描文件夹时,**app/Language** 将被跳过。 + +生成的语言文件很可能不符合你的编码标准。建议对它们进行格式化。例如,如果安装了 ``php-cs-fixer``,可以运行 ``vendor/bin/php-cs-fixer fix ./app/Language``。 + +在更新之前,可以预览命令找到的翻译: + +.. code-block:: console + + php spark lang:find --verbose --show-new + +``--verbose`` 的详细输出还会显示无效键的列表。例如: + +.. code-block:: console + + ... + + Files found: 10 + New translates found: 30 + Bad translates found: 5 + +------------------------+---------------------------------+ + | Bad Key | Filepath | + +------------------------+---------------------------------+ + | ..invalid_nested_key.. | app/Controllers/Translation.php | + | .invalid_key | app/Controllers/Translation.php | + | TranslationBad | app/Controllers/Translation.php | + | TranslationBad. | app/Controllers/Translation.php | + | TranslationBad... | app/Controllers/Translation.php | + +------------------------+---------------------------------+ + + All operations done! + +为了更准确的搜索,可以指定所需的语言环境或要扫描的目录。 + +.. code-block:: console + + php spark lang:find --dir Controllers/Translation --locale en --show-new + +详细信息可以通过运行以下命令找到: + +.. code-block:: console + + php spark lang:find --help diff --git a/source/outgoing/localization/019.php b/source/outgoing/localization/019.php new file mode 100644 index 00000000..5de7d9a8 --- /dev/null +++ b/source/outgoing/localization/019.php @@ -0,0 +1,13 @@ + [ + 'success' => 'Text.info.success', + ], + 'paragraph' => 'Text.paragraph', +]; diff --git a/source/outgoing/response.rst b/source/outgoing/response.rst index 6fe8b188..de03aaea 100644 --- a/source/outgoing/response.rst +++ b/source/outgoing/response.rst @@ -335,7 +335,7 @@ HTTP 缓存 告诉响应将所有内容发送回客户端。这将首先发送 header,然后是响应 body。对于主应用程序响应,你不需要调用它,因为 CodeIgniter 会自动处理。 - .. php:method:: setCookie($name = ''[, $value = ''[, $expire = ''[, $domain = ''[, $path = '/'[, $prefix = ''[, $secure = false[, $httponly = false[, $samesite = null]]]]]]]]) + .. php:method:: setCookie($name = ''[, $value = ''[, $expire = 0[, $domain = ''[, $path = '/'[, $prefix = ''[, $secure = false[, $httponly = false[, $samesite = null]]]]]]]]) :param array|Cookie|string $name: Cookie 名称 *或* 包含此方法可用的所有参数的关联数组 *或* ``CodeIgniter\Cookie\Cookie`` 的实例。 :param string $value: Cookie 值 @@ -361,7 +361,7 @@ HTTP 缓存 .. literalinclude:: response/023.php - 仅 ``name`` 和 ``value`` 是必需的。要删除 cookie,请将 ``expire`` 置空。 + 仅 ``name`` 和 ``value`` 是必需的。要删除 cookie,请将 ``value`` 置空。 ``expire`` 以 **秒** 设置,将添加到当前时间。不要包括时间,而只设置从 *现在* 希望 cookie 有效的秒数。如果 ``expire`` 设置为零,cookie 将只在浏览器打开时有效。 diff --git a/source/outgoing/response/011.php b/source/outgoing/response/011.php deleted file mode 100644 index bfac115c..00000000 --- a/source/outgoing/response/011.php +++ /dev/null @@ -1,12 +0,0 @@ -response->getCSP(); - -// specify the default directive treatment -$csp->reportOnly(false); - -// specify the origin to use if none provided for a directive -$csp->setDefaultSrc('cdn.example.com'); - -// specify the URL that "report-only" reports get sent to -$csp->setReportURI('http://example.com/csp/reports'); - -// specify that HTTP requests be upgraded to HTTPS -$csp->upgradeInsecureRequests(true); - -// add types or origins to CSP directives -// assuming that the default treatment is to block rather than just report -$csp->addBaseURI('example.com', true); // report only -$csp->addChildSrc('https://youtube.com'); // blocked -$csp->addConnectSrc('https://*.facebook.com', false); // blocked -$csp->addFontSrc('fonts.example.com'); -$csp->addFormAction('self'); -$csp->addFrameAncestor('none', true); // report this one -$csp->addImageSrc('cdn.example.com'); -$csp->addMediaSrc('cdn.example.com'); -$csp->addManifestSrc('cdn.example.com'); -$csp->addObjectSrc('cdn.example.com', false); // reject from here -$csp->addPluginType('application/pdf', false); // reject this media type -$csp->addScriptSrc('scripts.example.com', true); // allow but report requests from here -$csp->addStyleSrc('css.example.com'); -$csp->addSandbox(['allow-forms', 'allow-scripts']); diff --git a/source/outgoing/response/013.php b/source/outgoing/response/013.php deleted file mode 100644 index a34b3460..00000000 --- a/source/outgoing/response/013.php +++ /dev/null @@ -1,9 +0,0 @@ -response->getCSP(); - -$csp->addChildSrc('https://youtube.com'); // allowed -$csp->reportOnly(true); -$csp->addChildSrc('https://metube.com'); // allowed but reported -$csp->addChildSrc('https://ourtube.com', false); // allowed diff --git a/source/outgoing/response/023.php b/source/outgoing/response/023.php index 3d085573..dd3d905a 100644 --- a/source/outgoing/response/023.php +++ b/source/outgoing/response/023.php @@ -3,7 +3,7 @@ $cookie = [ 'name' => 'The Cookie Name', 'value' => 'The Value', - 'expire' => '86500', + 'expire' => 86500, 'domain' => '.some-domain.com', 'path' => '/', 'prefix' => 'myprefix_', diff --git a/source/outgoing/view_renderer.rst b/source/outgoing/view_renderer.rst index cf333836..1439a27d 100644 --- a/source/outgoing/view_renderer.rst +++ b/source/outgoing/view_renderer.rst @@ -99,13 +99,16 @@ View 类在内部使用关联数组来累积视图参数,直到你调用它的 ` 视图渲染器选项 ===================== -可以将几个选项传递给 ``render()`` 或 ``renderString()`` 方法: +可以将多个选项传递给 ``render()`` 或 ``renderString()`` 方法: -- ``cache`` - 以秒为单位,保存视图结果的时间;对 renderString() 忽略 -- ``cache_name`` - 用于保存/检索缓存视图结果的 ID;默认为视图路径;对 renderString() 忽略 -- ``saveData`` - 如果为 true,视图数据参数应保留以供随后的调用 +- ``$options`` -.. note:: 接口要求 ``saveData()`` 必须是布尔值,但实现类(如下面的 ``View``)可以扩展它以包含 ``null`` 值。 + - ``cache`` - 缓存视图结果的时间(以秒为单位);对于 ``renderString()`` 无效。 + - ``cache_name`` - 用于保存/检索缓存视图结果的 ID;默认为 ``$viewPath``;对于 ``renderString()`` 无效。 + - ``debug`` - 可以设置为 false 以禁用 :ref:`调试工具栏 ` 的调试代码添加。 +- ``$saveData`` - 如果视图数据参数应保留以供后续调用,则为 true。 + +.. note:: 接口定义的 ``$saveData`` 必须是布尔值,但实现类(如下面的 ``View``)可以扩展为包括 ``null`` 值。 *************** 类参考 diff --git a/source/outgoing/views.rst b/source/outgoing/views.rst index c648b42a..9d89c9ea 100644 --- a/source/outgoing/views.rst +++ b/source/outgoing/views.rst @@ -33,7 +33,7 @@ 显示视图 ================= -要加载和显示特定的视图文件,你需要在控制器中使用以下代码: +要加载和显示特定的视图文件,你可以在控制器中使用 :php:func:`view()` 函数,如以下代码所示: .. literalinclude:: views/001.php :lines: 2- @@ -59,7 +59,9 @@ 加载多个视图 ====================== -如果控制器内有多个 ``view()`` 调用,CodeIgniter 将智能地处理它们。如果发生多次调用,它们将被追加在一起。例如,你可能希望有一个头部视图、菜单视图、内容视图和页脚视图。可能如下所示: +CodeIgniter 会智能地处理控制器内对 :php:func:`view()` 的多次调用。如果发生多次调用,它们将被拼接在一起。 + +例如,你可能希望有一个头部视图、一个菜单视图、一个内容视图和一个底部视图。代码可能如下所示: .. literalinclude:: views/003.php @@ -92,7 +94,7 @@ 缓存视图 ============= -你可以通过在第三个参数中传递 ``cache`` 选项和缓存视图的秒数来缓存 ``view()`` 函数的视图: +你可以通过在 :php:func:`view()` 函数的第三个参数中传递一个 ``cache`` 选项,并指定缓存视图的秒数来缓存视图: .. literalinclude:: views/006.php :lines: 2- @@ -105,7 +107,9 @@ 向视图添加动态数据 =============================== -通过 ``view()`` 函数的第二个参数中的数组,控制器的数据传递给视图。这里有个例子: +数据通过 :php:func:`view()` 函数的第二个参数以数组的形式从控制器传递到视图。 + +这里有个例子: .. literalinclude:: views/008.php :lines: 2- @@ -123,7 +127,7 @@ saveData 选项 ------------------- -传入的数据在对 ``view()`` 的后续调用中保留。如果在单个请求中多次调用该函数,则不必为每个 ``view()`` 传递所需的数据。 +传递的数据在后续对 :php:func:`view()` 的调用中会被保留。如果你在单个请求中多次调用该函数,你不需要每次都将所需数据传递给每个 ``view()``。 但这可能无法防止任何数据“渗透”到其他视图中,从而潜在地造成问题。如果你更喜欢在一次调用后清除数据,可以将 ``saveData`` 选项传递到第三个参数中的 ``$option`` 数组中。 diff --git a/source/testing/cli.rst b/source/testing/cli.rst new file mode 100644 index 00000000..bf221ce9 --- /dev/null +++ b/source/testing/cli.rst @@ -0,0 +1,144 @@ +#################### +测试 CLI 命令 +#################### + +.. contents:: + :local: + :depth: 3 + +.. _using-mock-input-output: + +********************* +使用 MockInputOutput +********************* + +.. versionadded:: 4.5.0 + +MockInputOutput +=============== + +**MockInputOutput** 提供了一种简单的方法来编写需要用户输入的命令的测试,例如 ``CLI::prompt()``、``CLI::wait()`` 和 ``CLI::input()``。 + +你可以在测试执行期间用 ``MockInputOutput`` 替换 ``InputOutput`` 类来捕获输入和输出。 + +.. note:: 当你使用 ``MockInputOutput`` 时,你不需要使用 + :ref:`stream-filter-trait`、:ref:`ci-test-stream-filter` 和 + :ref:`php-stream-wrapper`。 + +辅助方法 +--------------- + +getOutput(?int $index = null): string +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +获取输出。 + +- 如果你像 ``$io->getOutput()`` 这样调用它,它会返回整个输出字符串。 +- 如果你指定 ``0`` 或一个正数,它会返回输出数组项。每个项都有一个 ``CLI::fwrite()`` 调用的输出。 +- 如果你指定一个负数 ``-n``,它会返回输出数组的倒数第 ``n`` 项。 + +getOutputs(): array +^^^^^^^^^^^^^^^^^^^ + +返回输出数组。每个项都有一个 ``CLI::fwrite()`` 调用的输出。 + +如何使用 +========== + +- ``CLI::setInputOutput()`` 可以将 ``MockInputOutput`` 实例设置到 ``CLI`` 类。 +- ``CLI::resetInputOutput()`` 重置 ``CLI`` 类中的 ``InputOutput`` 实例。 +- ``MockInputOutput::setInputs()`` 设置用户输入数组。 +- ``MockInputOutput::getOutput()`` 获取命令输出。 + +以下测试代码用于测试命令 ``spark db:table``: + +.. literalinclude:: cli/001.php + +*********************** +不使用 MockInputOutput +*********************** + +.. _testing-cli-output: + +测试 CLI 输出 +================== + +.. _stream-filter-trait: + +StreamFilterTrait +----------------- + +.. versionadded:: 4.3.0 + +**StreamFilterTrait** 提供了这些辅助方法的替代方案。 + +你可能需要测试一些难以测试的东西。有时,捕获一个流,比如 PHP 自己的 STDOUT 或 STDERR,可能会有所帮助。``StreamFilterTrait`` 帮助你捕获所选流的输出。 + +如何使用 +^^^^^^^^^^ + +- ``StreamFilterTrait::getStreamFilterBuffer()`` 从缓冲区获取捕获的数据。 +- ``StreamFilterTrait::resetStreamFilterBuffer()`` 重置捕获的数据。 + +在你的一个测试用例中演示这一点的示例: + +.. literalinclude:: overview/018.php + +``StreamFilterTrait`` 有一个自动调用的配置器。 +参见 :ref:`Testing Traits `。 + +如果你在测试中重写了 ``setUp()`` 或 ``tearDown()`` 方法,那么你必须分别调用 ``parent::setUp()`` 和 ``parent::tearDown()`` 方法来配置 ``StreamFilterTrait``。 + +.. _ci-test-stream-filter: + +CITestStreamFilter +------------------ + +**CITestStreamFilter** 用于手动/单次使用。 + +如果你只需要在一个测试中捕获流,那么可以手动向流添加过滤器,而不是使用 StreamFilterTrait trait。 + +如何使用 +^^^^^^^^^^ + +- ``CITestStreamFilter::registration()`` 过滤器注册。 +- ``CITestStreamFilter::addOutputFilter()`` 向输出流添加过滤器。 +- ``CITestStreamFilter::addErrorFilter()`` 向错误流添加过滤器。 +- ``CITestStreamFilter::removeOutputFilter()`` 从输出流中移除过滤器。 +- ``CITestStreamFilter::removeErrorFilter()`` 从错误流中移除过滤器。 + +.. literalinclude:: overview/020.php + +.. _testing-cli-input: + +测试 CLI 输入 +================= + +.. _php-stream-wrapper: + +PhpStreamWrapper +---------------- + +.. versionadded:: 4.3.0 + +**PhpStreamWrapper** 提供了一种方法来编写需要用户输入的方法的测试,例如 ``CLI::prompt()``、``CLI::wait()`` 和 ``CLI::input()``。 + +.. note:: PhpStreamWrapper 是一个流包装类。 + 如果你不了解 PHP 的流包装器, + 请参见 PHP 手册中的 `The streamWrapper class `_。 + +如何使用 +^^^^^^^^^^ + +- ``PhpStreamWrapper::register()`` 将 ``PhpStreamWrapper`` 注册到 ``php`` 协议。 +- ``PhpStreamWrapper::restore()`` 将 php 协议包装器恢复为 PHP 内置包装器。 +- ``PhpStreamWrapper::setContent()`` 设置输入数据。 + +.. important:: PhpStreamWrapper 仅用于测试 ``php://stdin``。 + 但是当你注册它时,它会处理所有 `php 协议 `_ 流, + 例如 ``php://stdout``、``php://stderr``、``php://memory``。 + 因此强烈建议仅在需要时注册/注销 ``PhpStreamWrapper``。否则,它在注册时会干扰其他内置的 php 流。 + +在你的一个测试用例中演示这一点的示例: + +.. literalinclude:: overview/019.php diff --git a/source/testing/cli/001.php b/source/testing/cli/001.php new file mode 100644 index 00000000..c0121204 --- /dev/null +++ b/source/testing/cli/001.php @@ -0,0 +1,37 @@ +setInputs(['a', '0']); + + command('db:table'); + + // Get the whole output string. + $output = $io->getOutput(); + + $expected = 'Which table do you want to see? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]: a'; + $this->assertStringContainsString($expected, $output); + + $expected = 'Data of Table "db_migrations":'; + $this->assertStringContainsString($expected, $output); + + // Remove MockInputOutput. + CLI::resetInputOutput(); + } +} diff --git a/source/testing/controllers/001.php b/source/testing/controllers/001.php index 9162fa0a..ae51187f 100644 --- a/source/testing/controllers/001.php +++ b/source/testing/controllers/001.php @@ -1,12 +1,12 @@ withURI('http://example.com/categories') - ->controller(\App\Controllers\ForumController::class) + ->controller(ForumController::class) ->execute('showCategories'); $this->assertTrue($result->isOK()); diff --git a/source/testing/controllers/011.php b/source/testing/controllers/011.php index f2ab558b..e53aa42c 100644 --- a/source/testing/controllers/011.php +++ b/source/testing/controllers/011.php @@ -1,11 +1,11 @@ `_ PHP 调试工具使调试更便利。Kint 比常规工具提供了更多功能,如将时间戳格式化为可识别日期,将十六进制代码显示为颜色,以表格形式展示数组数据方便阅读等等。 +.. _codeigniter-error-logs: + +CodeIgniter 错误日志 +====================== + +CodeIgniter 根据 **app/Config/Logger.php** 中的设置记录错误信息。 + +默认配置下,日志文件每天存储在 **writable/logs** 目录中。 +如果事情没有按预期进行,检查这些日志是个好主意! + +你可以调整错误阈值以查看更多或更少的信息。详情请参见 +:ref:`Logging `。 + +记录所有 SQL 查询 +======================= + +CodeIgniter 发出的所有 SQL 查询都可以被记录。 +详情请参见 :ref:`Database Events `。 + +******************** +替换 var_dump() +******************** + +虽然使用 Xdebug 和一个好的 IDE 对调试你的应用程序是不可或缺的, +但有时一个简单的 ``var_dump()`` 就足够了。CodeIgniter 通过捆绑 +优秀的 PHP 调试工具 `Kint `_ 使这一点变得更好。 + +这远远超出了你通常的工具,提供了许多替代数据, +例如将时间戳格式化为可识别的日期,显示颜色的十六进制代码, +将数组数据显示为易于阅读的表格,等等。 启用 Kint ============= @@ -26,6 +55,7 @@ d() ``d()`` 方法将传入的唯一参数的数据全部输出到屏幕,允许脚本继续执行: .. literalinclude:: debugging/001.php + :lines: 2- dd() ---- @@ -38,14 +68,15 @@ trace() 这会以 Kint 独特的方式提供当前执行点的回溯: .. literalinclude:: debugging/002.php + :lines: 2- 更多信息请参考 `Kint 文档 `_。 .. _the-debug-toolbar: -================= +***************** 调试工具栏 -================= +***************** 调试工具栏可以一目了然地查看当前页面请求的信息,包括基准测试结果、执行的查询、请求和响应数据等。这在开发过程中有助于调试和优化。 @@ -58,7 +89,9 @@ trace() .. note:: 当你的 ``baseURL`` 设置(在 **app/Config/App.php** 或 ``app.baseURL`` 在 **.env** 中)与实际 URL 不匹配时,不会显示调试工具栏。 -工具栏本身作为 :doc:`After 过滤器 ` 显示。可以通过在 **app/Config/Filters.php** 的 ``$globals`` 属性中删除它来完全禁用。 +工具栏本身显示为一个 :doc:`后置过滤器 `。你可以通过从 **app/Config/Filters.php** 文件的 ``$required``(或 ``$globals``)属性中移除 ``'toolbar'`` 来阻止它运行。 + +.. note:: 在 v4.5.0 之前,工具栏默认设置为 ``$globals``。 选择显示内容 --------------------- diff --git a/source/testing/fabricator.rst b/source/testing/fabricator.rst index 70ea5a86..620cb8bc 100644 --- a/source/testing/fabricator.rst +++ b/source/testing/fabricator.rst @@ -2,7 +2,7 @@ 生成测试数据 #################### -测试应用程序的时候,你经常需要一些示例数据。``Fabricator`` 类使用 fzaninotto 的 `Faker `_ 将模型转化为随机数据生成器。在种子文件或测试用例中使用 fabricator 来准备假数据用于单元测试。 +通常,你需要示例数据来运行应用程序的测试。``Fabricator`` 类使用 `Faker `_ 将模型转换为随机数据生成器。在你的种子或测试用例中使用 fabricators 来为单元测试准备虚假数据。 .. contents:: :local: @@ -45,10 +45,28 @@ Faker 通过从 formatter 请求数据来生成数据。如果没有定义 forma .. literalinclude:: fabricator/005.php 请注意,在这个例子中,前三个值等效于之前的 formatter。但是对于 ``avatar`` 我们请求了与默认不同的图像大小, ``login`` 使用基于应用配置的条件,这两者在使用 ``$formatters`` 参数时都是不可能的。 -你可能希望将测试数据与生产模型分开,所以最好是在测试支持文件夹中定义一个子类: + +你可能希望将测试数据与生产模型分开,所以最好是在测试支持文件夹中定义一个子类: .. literalinclude:: fabricator/006.php +设置修饰符 +================= + +.. versionadded:: 4.5.0 + +Faker 提供了三个特殊的提供者,``unique()``, ``optional()`` 和 ``valid()``,可以在任何提供者之前调用。Fabricator 通过提供专用方法完全支持这些修饰符。 + +.. literalinclude:: fabricator/022.php + +在字段名称之后传递的参数会直接按原样传递给修饰符。你可以参考 `Faker 的修饰符文档`_ 了解详细信息。 + +.. _Faker 的修饰符文档: https://fakerphp.github.io/#modifiers + +如果你在模型上使用 ``fake()`` 方法,你可以直接使用 Faker 的修饰符,而不是在 Fabricator 上调用每个方法。 + +.. literalinclude:: fabricator/023.php + 本地化 ============ diff --git a/source/testing/fabricator/001.php b/source/testing/fabricator/001.php index 8fbf1360..f237d56b 100644 --- a/source/testing/fabricator/001.php +++ b/source/testing/fabricator/001.php @@ -6,5 +6,15 @@ class MyModel implements FabricatorModel { + public function find($id = null) + { + // TODO: Implement find() method. + } + + public function insert($row = null, bool $returnID = true) + { + // TODO: Implement insert() method. + } + // ... } diff --git a/source/testing/fabricator/005.php b/source/testing/fabricator/005.php index c61fa527..5a601ceb 100644 --- a/source/testing/fabricator/005.php +++ b/source/testing/fabricator/005.php @@ -2,6 +2,8 @@ namespace App\Models; +use Faker\Generator; + class UserModel { // ... @@ -9,10 +11,10 @@ class UserModel public function fake(Generator &$faker) { return [ - 'first' => $faker->firstName, - 'email' => $faker->email, - 'phone' => $faker->phoneNumber, - 'avatar' => Faker\Provider\Image::imageUrl(800, 400), + 'first' => $faker->firstName(), + 'email' => $faker->email(), + 'phone' => $faker->phoneNumber(), + 'avatar' => \Faker\Provider\Image::imageUrl(800, 400), 'login' => config('Auth')->allowRemembering ? date('Y-m-d') : null, ]; @@ -20,10 +22,10 @@ public function fake(Generator &$faker) * Or you can return a return type object. return new User([ - 'first' => $faker->firstName, - 'email' => $faker->email, - 'phone' => $faker->phoneNumber, - 'avatar' => Faker\Provider\Image::imageUrl(800, 400), + 'first' => $faker->firstName(), + 'email' => $faker->email(), + 'phone' => $faker->phoneNumber(), + 'avatar' => \Faker\Provider\Image::imageUrl(800, 400), 'login' => config('Auth')->allowRemembering ? date('Y-m-d') : null, ]); diff --git a/source/testing/fabricator/006.php b/source/testing/fabricator/006.php index 56f34781..4cbe439a 100644 --- a/source/testing/fabricator/006.php +++ b/source/testing/fabricator/006.php @@ -3,10 +3,11 @@ namespace Tests\Support\Models; use App\Models\UserModel; +use Faker\Generator; class UserFabricator extends UserModel { - public function fake(&$faker) + public function fake(Generator &$faker) { // ... } diff --git a/source/testing/fabricator/008.php b/source/testing/fabricator/008.php index 1a0d56cf..e39dbe52 100644 --- a/source/testing/fabricator/008.php +++ b/source/testing/fabricator/008.php @@ -1,7 +1,8 @@ make(); print_r($testUser); diff --git a/source/testing/fabricator/021.php b/source/testing/fabricator/021.php index 236dd553..af6297f5 100644 --- a/source/testing/fabricator/021.php +++ b/source/testing/fabricator/021.php @@ -2,6 +2,9 @@ namespace App\Models; +use CodeIgniter\Test\Fabricator; +use Faker\Generator; + class UserModel { protected $table = 'users'; @@ -9,8 +12,8 @@ class UserModel public function fake(Generator &$faker) { return [ - 'first' => $faker->firstName, - 'email' => $faker->email, + 'first' => $faker->firstName(), + 'email' => $faker->email(), 'group_id' => mt_rand(1, Fabricator::getCount('groups')), ]; } diff --git a/source/testing/fabricator/022.php b/source/testing/fabricator/022.php new file mode 100644 index 00000000..dbde0ff3 --- /dev/null +++ b/source/testing/fabricator/022.php @@ -0,0 +1,11 @@ +setUnique('email'); // sets generated emails to be always unique +$fabricator->setOptional('group_id'); // sets group id to be optional, with 50% chance to be `null` +$fabricator->setValid('age', static fn (int $age): bool => $age >= 18); // sets age to be 18 and above only + +$users = $fabricator->make(10); diff --git a/source/testing/fabricator/023.php b/source/testing/fabricator/023.php new file mode 100644 index 00000000..2729e60d --- /dev/null +++ b/source/testing/fabricator/023.php @@ -0,0 +1,20 @@ + $faker->firstName(), + 'email' => $faker->unique()->email(), + 'group_id' => $faker->optional()->passthrough(mt_rand(1, Fabricator::getCount('groups'))), + ]; + } +} diff --git a/source/testing/feature.rst b/source/testing/feature.rst index 7678f559..050c34dc 100644 --- a/source/testing/feature.rst +++ b/source/testing/feature.rst @@ -23,13 +23,14 @@ HTTP 功能测试 基本上,功能测试允许你调用应用程序上的一个端点,并获取结果返回。 为此,你可以使用 ``call()`` 方法。 -1. 第一个参数是要使用的 HTTP 方法(通常是 GET 或 POST)。 +1. 第一个参数是要使用的 HTTP 方法(通常是 ``GET`` 或 ``POST``)。 2. 第二个参数是要测试的站点上的 URI 路径。 -3. 第三个参数 ``$params`` 接受一个数组,用于填充你正在使用的 HTTP 动词的超全局变量。因此,**GET** 方法将填充 **$_GET** 变量,而 **POST** 请求将填充 **$_POST** 数组。``$params`` 也用于 :ref:`feature-formatting-the-request`。 +3. 第三个参数 ``$params`` 接受一个数组,用于填充你正在使用的 HTTP 动词的超全局变量。因此,**GET** 方法将填充 ``$_GET`` 变量,而 **POST** 请求将填充 ``$_POST`` 数组。``$params`` 也用于 :ref:`feature-formatting-the-request`。 .. note:: ``$params`` 数组并不适用于每个 HTTP 动词,但为了保持一致性而包含在内。 .. literalinclude:: feature/002.php + :lines: 2- 缩写方法 ----------------- @@ -37,6 +38,7 @@ HTTP 功能测试 为每个 HTTP 动词提供了缩写方法,以减少输入并增加清晰度: .. literalinclude:: feature/003.php + :lines: 2- 设置不同的路由 ------------------------ @@ -44,6 +46,7 @@ HTTP 功能测试 你可以通过将“routes”数组传递到 ``withRoutes()`` 方法来使用自定义路由集合。这将覆盖系统中的任何现有路由: .. literalinclude:: feature/004.php + :lines: 2- 每个“routes”都是一个包含 HTTP 动词(或“add”表示全部)、要匹配的 URI 和路由目的地的 3 元素数组。 @@ -53,6 +56,7 @@ HTTP 功能测试 你可以使用 ``withSession()`` 方法在单次测试期间设置自定义会话值。这需要一个键/值对数组,在发出此请求时,它应存在于 ``$_SESSION`` 变量中,或者为 ``null`` 表示应使用 ``$_SESSION`` 的当前值。这在测试认证等方面很有用。 .. literalinclude:: feature/005.php + :lines: 2- 设置标头 --------------- @@ -60,6 +64,7 @@ HTTP 功能测试 你可以使用 ``withHeaders()`` 方法设置标头值。这需要一个键/值对数组,它将作为调用中的标头传递: .. literalinclude:: feature/006.php + :lines: 2- 绕过事件 ---------------- @@ -67,6 +72,7 @@ HTTP 功能测试 事件在应用程序中很有用,但在测试中可能 problematic。特别是用于发送电子邮件的事件。你可以使用 ``skipEvents()`` 方法告诉系统跳过任何事件处理: .. literalinclude:: feature/007.php + :lines: 2- .. _feature-formatting-the-request: @@ -81,6 +87,7 @@ HTTP 功能测试 这还将相应地设置请求的 `Content-Type` 标头。 .. literalinclude:: feature/008.php + :lines: 2- .. _feature-setting-the-body: diff --git a/source/testing/feature/001.php b/source/testing/feature/001.php index 0ab64046..9884535d 100644 --- a/source/testing/feature/001.php +++ b/source/testing/feature/001.php @@ -1,11 +1,12 @@ call('get', '/'); +$result = $this->call('GET', '/'); // Submit a form $result = $this->call('post', 'contact', [ diff --git a/source/testing/feature/004.php b/source/testing/feature/004.php index 4f98ced2..7e9a45d7 100644 --- a/source/testing/feature/004.php +++ b/source/testing/feature/004.php @@ -1,7 +1,7 @@ withRoutes($routes)->get('users'); diff --git a/source/testing/index.rst b/source/testing/index.rst index 48701a5a..1847e9b6 100644 --- a/source/testing/index.rst +++ b/source/testing/index.rst @@ -13,6 +13,7 @@ CodeIgniter 具备许多工具,可帮助你彻底测试和调试应用程序 测试控制器 HTTP 测试 response + cli + 模拟 benchmark debugging - 模拟 diff --git a/source/testing/overview.rst b/source/testing/overview.rst index 0ef08d20..f494a62e 100644 --- a/source/testing/overview.rst +++ b/source/testing/overview.rst @@ -52,20 +52,22 @@ Phar PHPUnit 配置 ===================== -项目根目录中有一个 ``phpunit.xml.dist`` 文件。它控制框架本身的单元测试。如果提供自己的 ``phpunit.xml``,则会覆盖它。 +在你的 CodeIgniter 项目根目录中,有一个 ``phpunit.xml.dist`` 文件。这个文件控制着你的应用程序的单元测试。如果你提供了自己的 ``phpunit.xml``,它将覆盖默认文件。 -如果正在对应用程序进行单元测试,则 ``phpunit.xml`` 应该排除 ``system`` 文件夹以及任何 ``vendor`` 或 ``ThirdParty`` 文件夹。 +默认情况下,测试文件放置在项目根目录下的 **tests** 目录中。 测试类 ============== -为了利用所提供的其他工具,测试必须扩展 ``CIUnitTestCase``。默认情况下,所有测试都预计位于 **tests/app** 目录中。 +为了利用提供的额外工具,你的测试必须继承 ``CodeIgniter\Test\CIUnitTestCase``。 -要测试新的库 **Foo**,你将在 **tests/app/Libraries/FooTest.php** 中创建一个新文件: +对于测试文件的放置位置没有硬性规定。然而,我们建议你提前制定放置规则,以便你能快速了解测试文件的位置。 + +在本文档中,我们将把与 **app** 目录中的类对应的测试文件放置在 **tests/app** 目录中。要测试一个新的库 **app/Libraries/Foo.php**,你需要在 **tests/app/Libraries/FooTest.php** 创建一个新文件: .. literalinclude:: overview/001.php -要测试模型之一,你最终可能会在 **tests/app/Models/OneOfMyModelsTest.php** 中以类似以下形式结束: +要测试你的某个模型 **app/Models/UserModel.php**,你可能会在 **tests/app/Models/UserModelTest.php** 中得到如下内容: .. literalinclude:: overview/002.php @@ -88,7 +90,7 @@ PHPUnit 配置 静态方法 ``setUpBeforeClass()`` 和 ``tearDownAfterClass()`` 分别在整个测试用例之前和之后运行,而受保护的方法 ``setUp()`` 和 ``tearDown()`` 在每个测试之间运行。 -如果实现了这些特殊函数中的任何一个,请确保也运行其父函数,以免扩展的测试用例干扰环境搭建: +如果你实现了这些特殊函数中的任何一个,请确保你也运行它们的父级函数,以免扩展的测试用例干扰到分阶段测试: .. literalinclude:: overview/003.php @@ -239,77 +241,6 @@ Services::resetSingle(string $name) .. literalinclude:: overview/021.php -你可以使用 ``Time::setTestNow()`` 方法来固定当前时间。 -你还可以选择性地将地区设置为第二个参数。 - -不要忘记在测试后通过调用它而不带参数来重置当前时间。 - -.. _testing-cli-output: - -测试 CLI 输出 -================== - -StreamFilterTrait ------------------ - -.. versionadded:: 4.3.0 - -**StreamFilterTrait** 提供了这些帮助方法的替代方法。 - -你可能需要测试一些难以测试的内容。有时,捕获流,如 PHP 自己的 STDOUT 或 STDERR,可能会有所帮助。``StreamFilterTrait`` 帮助你从选择的流中捕获输出。 - -**方法概览** - -- ``StreamFilterTrait::getStreamFilterBuffer()`` 获取缓冲区中的捕获数据。 -- ``StreamFilterTrait::resetStreamFilterBuffer()`` 重置捕获的数据。 - -在测试用例中演示此用法的示例: - -.. literalinclude:: overview/018.php - -``StreamFilterTrait`` 具有自动调用的配置器。请参阅 :ref:`Testing Traits `。 - -如果在测试中覆盖 ``setUp()`` 或 ``tearDown()`` 方法,则必须分别调用 ``parent::setUp()`` 和 ``parent::tearDown()`` 方法来配置 ``StreamFilterTrait``。 - -CITestStreamFilter ------------------- - -**CITestStreamFilter** 用于手动单次使用。 - -如果只需要在一个测试中捕获流,那么可以手动将过滤器添加到流,而不是使用 StreamFilterTrait trait。 - -**方法概览** - -- ``CITestStreamFilter::registration()`` 过滤器注册。 -- ``CITestStreamFilter::addOutputFilter()`` 向输出流添加过滤器。 -- ``CITestStreamFilter::addErrorFilter()`` 向错误流添加过滤器。 -- ``CITestStreamFilter::removeOutputFilter()`` 从输出流中移除过滤器。 -- ``CITestStreamFilter::removeErrorFilter()`` 从错误流中移除过滤器。 - -.. literalinclude:: overview/020.php - -.. _testing-cli-input: - -测试 CLI 输入 -================= - -PhpStreamWrapper ----------------- - -.. versionadded:: 4.3.0 - -**PhpStreamWrapper** 提供了测试需要用户输入的方法(如 ``CLI::prompt()``、``CLI::wait()`` 和 ``CLI::input()``)的途径。 - -.. note:: PhpStreamWrapper 是一个流包装器类。如果你不了解 PHP 的流包装器,请参阅 PHP 手册中的 `The streamWrapper class `_。 - -**方法概览** - -- ``PhpStreamWrapper::register()`` 将 ``PhpStreamWrapper`` 注册到 ``php`` 协议。 -- ``PhpStreamWrapper::restore()`` 将 php 协议包装器恢复为内置的 PHP 包装器。 -- ``PhpStreamWrapper::setContent()`` 设置输入数据。 - -.. important:: PhpStreamWrapper 仅用于测试 ``php://stdin``。但是在注册时,它会处理所有 `php 协议 `_ 流,例如 ``php://stdout``、``php://stderr``、``php://memory``。所以,强烈建议仅在需要时注册/取消注册 PhpStreamWrapper。否则,它会在注册期间干扰其他内置的 php 流。 - -在测试用例中演示用法的示例: +你可以使用 ``Time::setTestNow()`` 方法来固定当前时间。可选地,你可以指定一个语言环境作为第二个参数。 -.. literalinclude:: overview/019.php +不要忘记在测试后调用该方法(不带参数)来重置当前时间。 diff --git a/source/testing/overview/002.php b/source/testing/overview/002.php index eca8ef90..db746b62 100644 --- a/source/testing/overview/002.php +++ b/source/testing/overview/002.php @@ -4,7 +4,7 @@ use CodeIgniter\Test\CIUnitTestCase; -class OneOfMyModelsTest extends CIUnitTestCase +class UserModelTest extends CIUnitTestCase { public function testFooNotBar() { diff --git a/source/testing/overview/003.php b/source/testing/overview/003.php index fe9f1632..ee39231c 100644 --- a/source/testing/overview/003.php +++ b/source/testing/overview/003.php @@ -4,7 +4,7 @@ use CodeIgniter\Test\CIUnitTestCase; -final class OneOfMyModelsTest extends CIUnitTestCase +final class UserModelTest extends CIUnitTestCase { protected function setUp(): void { diff --git a/source/testing/overview/016.php b/source/testing/overview/016.php index d3a769d8..8598a4a7 100644 --- a/source/testing/overview/016.php +++ b/source/testing/overview/016.php @@ -1,5 +1,8 @@ getMockBuilder('CodeIgniter\HTTP\CURLRequest') - ->setMethods(['request']) + $curlrequest = $this->getMockBuilder(CURLRequest::class) + ->onlyMethods(['request']) ->getMock(); Services::injectMock('curlrequest', $curlrequest); diff --git a/source/testing/overview/017.php b/source/testing/overview/017.php index f5e43637..d989ed7e 100644 --- a/source/testing/overview/017.php +++ b/source/testing/overview/017.php @@ -2,6 +2,7 @@ namespace Tests; +use App\Models\UserModel; use CodeIgniter\Config\Factories; use CodeIgniter\Test\CIUnitTestCase; use Tests\Support\Mock\MockUserModel; @@ -13,6 +14,6 @@ protected function setUp(): void parent::setUp(); $model = new MockUserModel(); - Factories::injectMock('models', 'App\Models\UserModel', $model); + Factories::injectMock('models', UserModel::class, $model); } } diff --git a/source/testing/overview/018.php b/source/testing/overview/018.php index 09f1f4e4..cddef79a 100644 --- a/source/testing/overview/018.php +++ b/source/testing/overview/018.php @@ -1,5 +1,7 @@ seeXPath('//h1[contains(@class, "heading")]')) { + // ... +} + +// Check that h1 element which contains class "heading" have a text "Hello World" +if ($results->seeXPath('//h1[contains(@class, "heading")][contains(.,"Hello world")]')) { + // ... +} diff --git a/source/testing/response/034.php b/source/testing/response/034.php new file mode 100644 index 00000000..e900dd1f --- /dev/null +++ b/source/testing/response/034.php @@ -0,0 +1,11 @@ +dontSeeXPath('//h1[contains(@class, "heading")]')) { + // ... +} + +// Check that h1 element which contains class "heading" and text "Hello World" does NOT exist on the page +if ($results->dontSeeXPath('//h1[contains(@class, "heading")][contains(.,"Hello world")]')) { + // ... +} diff --git a/source/tutorial/create_news_items/001.php b/source/tutorial/create_news_items/001.php index 8c5ce750..3fc800af 100644 --- a/source/tutorial/create_news_items/001.php +++ b/source/tutorial/create_news_items/001.php @@ -9,7 +9,7 @@ class Filters extends BaseConfig // ... public $methods = [ - 'post' => ['csrf'], + 'POST' => ['csrf'], ]; // ... diff --git a/source/tutorial/news_section.rst b/source/tutorial/news_section.rst index d59f362e..bfe09ea0 100755 --- a/source/tutorial/news_section.rst +++ b/source/tutorial/news_section.rst @@ -68,7 +68,7 @@ CodeIgniter 安装假定你已经按 :ref:`要求 ` 在 ``CodeIgniter\Model`` 中使用。这使你可以编写一次'查询',并在 :doc:`所有支持的数据库系统 <../intro/requirements>` 上使用。Model 类也允许你轻松使用 Query Builder 并提供一些额外的工具,以简化使用数据。向模型添加以下代码。 .. literalinclude:: news_section/002.php - :lines: 11-18 + :lines: 11-23 使用此代码,你可以执行两种不同的查询。你可以获取所有新闻记录,也可以通过其 slug 获取新闻项。你可能已经注意到,在运行查询之前没有转义 ``$slug`` 变量; :doc:`查询构建器 <../database/query_builder>` 会为你完成这一步。 @@ -120,7 +120,7 @@ CodeIgniter 安装假定你已经按 :ref:`要求 ` 或第三方解析器。 diff --git a/source/tutorial/news_section/002.php b/source/tutorial/news_section/002.php index 5d4b11f7..3a17f428 100644 --- a/source/tutorial/news_section/002.php +++ b/source/tutorial/news_section/002.php @@ -8,6 +8,11 @@ class NewsModel extends Model { protected $table = 'news'; + /** + * @param false|string $slug + * + * @return array|null + */ public function getNews($slug = false) { if ($slug === false) { diff --git a/source/tutorial/news_section/003.php b/source/tutorial/news_section/003.php index 22b24577..fe5c0378 100644 --- a/source/tutorial/news_section/003.php +++ b/source/tutorial/news_section/003.php @@ -10,10 +10,10 @@ public function index() { $model = model(NewsModel::class); - $data['news'] = $model->getNews(); + $data['news_list'] = $model->getNews(); } - public function show($slug = null) + public function show(?string $slug = null) { $model = model(NewsModel::class); diff --git a/source/tutorial/news_section/004.php b/source/tutorial/news_section/004.php index 625acaf8..1f17215e 100644 --- a/source/tutorial/news_section/004.php +++ b/source/tutorial/news_section/004.php @@ -11,8 +11,8 @@ public function index() $model = model(NewsModel::class); $data = [ - 'news' => $model->getNews(), - 'title' => 'News archive', + 'news_list' => $model->getNews(), + 'title' => 'News archive', ]; return view('templates/header', $data) diff --git a/source/tutorial/news_section/005.php b/source/tutorial/news_section/005.php index 39db0a43..0fdfd46e 100644 --- a/source/tutorial/news_section/005.php +++ b/source/tutorial/news_section/005.php @@ -1,8 +1,8 @@

    - + - +

    diff --git a/source/tutorial/news_section/006.php b/source/tutorial/news_section/006.php index 95e48aa7..b848533c 100644 --- a/source/tutorial/news_section/006.php +++ b/source/tutorial/news_section/006.php @@ -9,13 +9,13 @@ class News extends BaseController { // ... - public function show($slug = null) + public function show(?string $slug = null) { $model = model(NewsModel::class); $data['news'] = $model->getNews($slug); - if (empty($data['news'])) { + if ($data['news'] === null) { throw new PageNotFoundException('Cannot find the news item: ' . $slug); } diff --git a/source/tutorial/static_pages.rst b/source/tutorial/static_pages.rst index e3868826..3290908d 100755 --- a/source/tutorial/static_pages.rst +++ b/source/tutorial/static_pages.rst @@ -112,7 +112,7 @@ CodeIgniter 从上到下读取其路由规则,并将请求路由到第一个 如果请求的页面不存在,将显示“404 页面未找到”错误。 -此方法中的第一行检查页面是否确实存在。使用 PHP 原生的 ``is_file()`` 函数来检查文件是否在预期的位置。``PageNotFoundException`` 是一个 CodeIgniter 异常,会导致显示默认错误页面。 +此方法的第一行检查页面是否实际存在。PHP 原生的 ``is_file()`` 函数用于检查文件是否在预期的位置。``PageNotFoundException`` 是一个 CodeIgniter 异常,它会导致显示 404 页面未找到错误页面。 在页眉模板中,使用 ``$title`` 变量来自定义页面标题。此方法中定义了 title 的值,但不是将值赋给变量,而是将其赋给 ``$data`` 数组中的 title 元素。 @@ -154,8 +154,8 @@ CodeIgniter 从上到下读取其路由规则,并将请求路由到第一个 | localhost:8080/pages | 来自我们的 ``Pages`` 控制器中的 ``index()`` 方法的结果, | | | 它显示 CodeIgniter “欢迎”页面。 | +---------------------------------+-----------------------------------------------------------------+ - | localhost:8080/home | 显示上面制作的“主页”,因为我们明确要求它。来自我们的 | - | | ``Pages`` 控制器中的 ``view()`` 方法的结果。 | + | localhost:8080/home | 因为我们明确请求了上面创建的 "home" 页面, | + | | 所以结果来自我们 ``Pages`` 控制器中的 ``view()`` 方法。 | +---------------------------------+-----------------------------------------------------------------+ | localhost:8080/about | 显示上面制作的“关于”页面,因为我们明确要求它。 | +---------------------------------+-----------------------------------------------------------------+ diff --git a/source/tutorial/static_pages/001.php b/source/tutorial/static_pages/001.php index 0cedb8e4..92960147 100644 --- a/source/tutorial/static_pages/001.php +++ b/source/tutorial/static_pages/001.php @@ -9,7 +9,7 @@ public function index() return view('welcome_message'); } - public function view($page = 'home') + public function view(string $page = 'home') { // ... } diff --git a/source/tutorial/static_pages/002.php b/source/tutorial/static_pages/002.php index 997f72b1..67ed7d95 100644 --- a/source/tutorial/static_pages/002.php +++ b/source/tutorial/static_pages/002.php @@ -2,13 +2,14 @@ namespace App\Controllers; -use CodeIgniter\Exceptions\PageNotFoundException; // Add this line +// Add this line to import the class. +use CodeIgniter\Exceptions\PageNotFoundException; class Pages extends BaseController { // ... - public function view($page = 'home') + public function view(string $page = 'home') { if (! is_file(APPPATH . 'Views/pages/' . $page . '.php')) { // Whoops, we don't have a page for that!